├── tests ├── issues │ ├── dummy.zip │ ├── 42.zip │ ├── poc0.zip │ ├── poc1.zip │ ├── zbsm.zip │ ├── issue34.zip │ ├── nasty.zip │ ├── unicode.zip │ ├── issue33_1.zip │ ├── issue33_2.zip │ ├── unicode2.zip │ ├── issue_05_1.zip │ ├── zip-slip-win.zip │ ├── zip-slip-linux.zip │ ├── issue_05_password.zip │ ├── issue_05_nopassword.zip │ └── README.md ├── main.cpp ├── Makefile ├── CMakeLists.txt ├── TestHelper.hpp ├── TestSecurity.cpp ├── TestZipMemory.cpp └── TestIssues.cpp ├── external ├── manifest └── compilation ├── doc ├── logo.png ├── doxygen-logo.png └── demos │ ├── CMakeHelloWorld │ ├── CMakeLists.txt │ └── main.cpp │ ├── README.md │ ├── Unzipper │ ├── CMakeLists.txt │ ├── Makefile │ └── src │ │ └── demo_unzip.cpp │ └── Zipper │ ├── CMakeLists.txt │ ├── Makefile │ └── src │ └── demo_zip.cpp ├── Makefile.common ├── .vscode ├── settings.json └── launch.json ├── src └── utils │ ├── glob.hpp │ ├── glob.cpp │ ├── Timestamp.hpp │ ├── OS.hpp │ ├── Timestamp.cpp │ ├── dirent.h │ ├── dirent.c │ └── Path.hpp ├── .gitmodules ├── zipper.pc.in ├── AUTHORS ├── .gitignore ├── zipperConfig.cmake.in ├── LICENSE ├── Makefile ├── ChangeLog ├── .clang-format ├── CMakeLists.txt ├── .github └── workflows │ └── ci.yaml ├── include └── Zipper │ ├── Unzipper.hpp │ └── Zipper.hpp └── README.md /tests/issues/dummy.zip: -------------------------------------------------------------------------------- 1 | PK -------------------------------------------------------------------------------- /external/manifest: -------------------------------------------------------------------------------- 1 | zlib-ng/zlib-ng@2.2.4 2 | Lecrapouille/minizip@v1.2 -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/doc/logo.png -------------------------------------------------------------------------------- /tests/issues/42.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/42.zip -------------------------------------------------------------------------------- /doc/doxygen-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/doc/doxygen-logo.png -------------------------------------------------------------------------------- /tests/issues/poc0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/poc0.zip -------------------------------------------------------------------------------- /tests/issues/poc1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/poc1.zip -------------------------------------------------------------------------------- /tests/issues/zbsm.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/zbsm.zip -------------------------------------------------------------------------------- /tests/issues/issue34.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/issue34.zip -------------------------------------------------------------------------------- /tests/issues/nasty.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/nasty.zip -------------------------------------------------------------------------------- /tests/issues/unicode.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/unicode.zip -------------------------------------------------------------------------------- /tests/issues/issue33_1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/issue33_1.zip -------------------------------------------------------------------------------- /tests/issues/issue33_2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/issue33_2.zip -------------------------------------------------------------------------------- /tests/issues/unicode2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/unicode2.zip -------------------------------------------------------------------------------- /tests/issues/issue_05_1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/issue_05_1.zip -------------------------------------------------------------------------------- /tests/issues/zip-slip-win.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/zip-slip-win.zip -------------------------------------------------------------------------------- /Makefile.common: -------------------------------------------------------------------------------- 1 | PROJECT_NAME := zipper 2 | PROJECT_VERSION := 3.0.2 3 | LICENSE := MIT 4 | COMPILATION_MODE := release -------------------------------------------------------------------------------- /tests/issues/zip-slip-linux.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/zip-slip-linux.zip -------------------------------------------------------------------------------- /tests/issues/issue_05_password.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/issue_05_password.zip -------------------------------------------------------------------------------- /tests/issues/issue_05_nopassword.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lecrapouille/zipper/HEAD/tests/issues/issue_05_nopassword.zip -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // clang-format 3 | "clang-format.executable": "clang-format", 4 | "editor.formatOnSave": true, 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/glob.hpp: -------------------------------------------------------------------------------- 1 | #ifndef GLOB_HPP 2 | #define GLOB_HPP 3 | 4 | #include 5 | #include 6 | 7 | std::regex globToRegex(const std::string& glob); 8 | 9 | #endif -------------------------------------------------------------------------------- /doc/demos/CMakeHelloWorld/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | project(zipper-cmake-example) 3 | set(CMAKE_CXX_STANDARD 14) 4 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 5 | find_package(zipper REQUIRED) 6 | add_executable(test_zipper main.cpp) 7 | target_link_libraries(test_zipper zipper::zipper) -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #include "gmock/gmock.h" 2 | #include "gtest/gtest.h" 3 | 4 | int main(int argc, char* argv[]) 5 | { 6 | // The following line must be executed to initialize Google Mock 7 | // (and Google Test) before running the tests. 8 | ::testing::InitGoogleMock(&argc, argv); 9 | 10 | return RUN_ALL_TESTS(); 11 | } 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".makefile"] 2 | path = .makefile 3 | url = https://github.com/Lecrapouille/MyMakefile.git 4 | [submodule "external/zlib-ng"] 5 | path = external/zlib-ng 6 | url = https://github.com/zlib-ng/zlib-ng.git 7 | [submodule "external/minizip"] 8 | path = external/minizip 9 | url = https://github.com/Lecrapouille/minizip 10 | -------------------------------------------------------------------------------- /zipper.pc.in: -------------------------------------------------------------------------------- 1 | prefix=@CMAKE_INSTALL_PREFIX@ 2 | exec_prefix=${prefix} 3 | libdir=${exec_prefix}/lib 4 | includedir=${prefix}/include 5 | 6 | Name: zipper 7 | Description: C++ wrapper around minizip for creating, extracting and updating zip archives 8 | Version: @PROJECT_VERSION@ 9 | Requires: 10 | Libs: -L${libdir} -lzipper 11 | Cflags: -I${includedir} -------------------------------------------------------------------------------- /doc/demos/CMakeHelloWorld/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() 5 | { 6 | zipper::Zipper zipper("test.zip"); 7 | 8 | std::stringstream content("Hello World!"); 9 | zipper.add(content, "hello.txt"); 10 | 11 | zipper.close(); 12 | 13 | std::cout << "Hello World zipper done !" << std::endl; 14 | return EXIT_SUCCESS; 15 | } -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Original project 2 | 3 | https://github.com/sebastiandev/zipper 4 | 5 | Main developers: 6 | - SBML Team 7 | - Sebastian (https://github.com/sebastiandev) 8 | 9 | # Forked project 10 | 11 | https://github.com/Lecrapouille/zipper 12 | 13 | Main developer: 14 | - Quentin Quadrat 15 | 16 | Contributors: 17 | - LecrisUT (https://github.com/LecrisUT): Fix CMake 18 | - Michael Wutzig (https://github.com/wutzig): CMake for Windows 19 | - mq白 (https://github.com/Mq-b): Minor fixes -------------------------------------------------------------------------------- /tests/issues/README.md: -------------------------------------------------------------------------------- 1 | # Note concerning issues 2 | 3 | - issue33_1.zip, issue33_2.zip and issue34.zip refers to https://github.com/sebastiandev/zipper/issues 4 | - issue_05.zip refers to https://github.com/Lecrapouille/zipper/issues 5 | - poc0.zip and poc1.zip come from https://github.com/zlib-ng/minizip-ng/issues/739 coming from https://nvd.nist.gov/vuln/detail/CVE-2023-48107 6 | - zip-slip-win.zip, zip-slip-linux.zip: https://github.com/snyk/zip-slip-vulnerability/tree/master 7 | - unicode.zip, unicode2.zip: https://github.com/corkami/pocs/tree/master/zip 8 | - 42.zip: https://unforgettable.dk/ 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | # Prerequisites 4 | *.d 5 | 6 | # Compiled Object files 7 | *.slo 8 | *.lo 9 | *.o 10 | *.obj 11 | 12 | # Precompiled Headers 13 | *.gch 14 | *.pch 15 | 16 | # Compiled Dynamic libraries 17 | *.so 18 | *.dylib 19 | *.dll 20 | 21 | # Fortran module files 22 | *.mod 23 | *.smod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | 36 | # Project 37 | build 38 | tests/build 39 | doc/coverage 40 | doc/html 41 | doc/doxygen 42 | doc/demos/*/VERSION* 43 | 44 | # apitrace traces (opengl) 45 | *.trace 46 | -------------------------------------------------------------------------------- /zipperConfig.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | # Zipper CMake Configuration File 4 | # This file is configured during the build process to help find_package(zipper) work 5 | 6 | # Set the version 7 | set(ZIPPER_VERSION @PROJECT_VERSION@) 8 | 9 | # Include the target definitions 10 | include("${CMAKE_CURRENT_LIST_DIR}/zipperTargets.cmake") 11 | 12 | # Set the include directories 13 | set_and_check(ZIPPER_INCLUDE_DIRS "@PACKAGE_INCLUDE_INSTALL_DIR@") 14 | 15 | # Set the libraries 16 | set(ZIPPER_LIBRARIES zipper::zipper) 17 | 18 | # Check that all required components are available 19 | check_required_components(zipper) -------------------------------------------------------------------------------- /doc/demos/README.md: -------------------------------------------------------------------------------- 1 | # Basic standalone application 2 | 3 | To compile the standalone unzipper demo: 4 | 5 | ``` 6 | cd Unzipper 7 | make -j8 8 | ./build/demo_unzip -p -f -o /tmp ../../../tests/issues/issue_05_password.zip 9 | ``` 10 | 11 | Type `1234` as password in the prompt and for this given example. 12 | Beware password are displayed on the console. 13 | 14 | Commande line: 15 | - `-p` for asking you to type a password in the case your zip file has been encrypted. 16 | - `-f` to force smashing files when extracting. 17 | - `-o` path to extract data, if not given the zip is extracted at `.`. 18 | - `file.zip` shall be given at the last position of the command line. 19 | - `-h` display the help. 20 | -------------------------------------------------------------------------------- /external/compilation: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | ### Library zlib-ng 3 | ############################################################################### 4 | print-compile zlib-ng 5 | mkdir -p zlib-ng/build 6 | (cd zlib-ng/build 7 | call-cmake -DZLIB_COMPAT=ON -DZLIB_ENABLE_TESTS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON .. 8 | call-make 9 | ) 10 | 11 | ############################################################################### 12 | ### Library minizip 13 | ############################################################################### 14 | print-compile minizip 15 | mkdir -p minizip/build 16 | (cd minizip/build 17 | call-cmake -DUSE_AES=ON .. 18 | call-make 19 | ) -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug ", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/build/zipper-tests", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${workspaceFolder}", 12 | "environment": [], 13 | "externalConsole": true, 14 | "MIMode": "gdb", 15 | "setupCommands": [ 16 | { 17 | "description": "Enable pretty-printing for gdb", 18 | "text": "-enable-pretty-printing", 19 | "ignoreFailures": false 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /doc/demos/Unzipper/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Unzipper Demo CMake Configuration 3 | ############################################################################## 4 | 5 | set(UNZIPPER_DEMO_SOURCES 6 | src/demo_unzip.cpp 7 | ) 8 | 9 | # Define the demo executable 10 | add_executable(unzipper-demo ${UNZIPPER_DEMO_SOURCES}) 11 | 12 | # Configure the include directories 13 | target_include_directories(unzipper-demo PRIVATE 14 | ${CMAKE_SOURCE_DIR} 15 | ${CMAKE_SOURCE_DIR}/include 16 | ${CMAKE_SOURCE_DIR}/src 17 | ) 18 | 19 | # Define important macros 20 | target_compile_definitions(unzipper-demo PRIVATE 21 | HAVE_AES 22 | ) 23 | 24 | # Link with the main library 25 | target_link_libraries(unzipper-demo PRIVATE 26 | zipper 27 | ) -------------------------------------------------------------------------------- /doc/demos/Zipper/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Zipper Demo CMake Configuration 3 | ############################################################################## 4 | 5 | set(ZIPPER_DEMO_SOURCES 6 | src/demo_zip.cpp 7 | ) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | 11 | # Define the demo executable 12 | add_executable(zipper-demo ${ZIPPER_DEMO_SOURCES}) 13 | 14 | # Configure the include directories 15 | target_include_directories(zipper-demo PRIVATE 16 | ${CMAKE_SOURCE_DIR} 17 | ${CMAKE_SOURCE_DIR}/include 18 | ${CMAKE_SOURCE_DIR}/src 19 | ) 20 | 21 | # Define important macros 22 | target_compile_definitions(zipper-demo PRIVATE 23 | HAVE_AES 24 | ) 25 | 26 | # Link with the main library 27 | target_link_libraries(zipper-demo PRIVATE 28 | zipper 29 | ) -------------------------------------------------------------------------------- /doc/demos/Zipper/Makefile: -------------------------------------------------------------------------------- 1 | ################################################### 2 | # Location of the project directory and Makefiles 3 | # 4 | P := ../../.. 5 | M := $(P)/.makefile 6 | 7 | ################################################### 8 | # Project definition 9 | # 10 | include $(P)/Makefile.common 11 | TARGET_NAME := zipper-demo 12 | TARGET_DESCRIPTION := A demo for zipping using $(PROJECT_NAME) 13 | CXX_STANDARD := --std=c++17 14 | include $(M)/project/Makefile 15 | 16 | ################################################### 17 | # Compile the standalone application 18 | # 19 | SRC_FILES := $(call rwildcard,src,*.cpp) 20 | INCLUDES := $(P)/include src 21 | VPATH := $(P)/demo 22 | INTERNAL_LIBS := $(call internal-lib,$(PROJECT_NAME)) 23 | 24 | ################################################### 25 | # Generic Makefile rules 26 | # 27 | include $(M)/rules/Makefile 28 | 29 | $(INTERNAL_LIBS): -------------------------------------------------------------------------------- /doc/demos/Unzipper/Makefile: -------------------------------------------------------------------------------- 1 | ################################################### 2 | # Location of the project directory and Makefiles 3 | # 4 | P := ../../.. 5 | M := $(P)/.makefile 6 | 7 | ################################################### 8 | # Project definition 9 | # 10 | include $(P)/Makefile.common 11 | TARGET_NAME := unzipper-demo 12 | TARGET_DESCRIPTION := A demo for unzipping using $(PROJECT_NAME) 13 | CXX_STANDARD := --std=c++17 14 | include $(M)/project/Makefile 15 | 16 | ################################################### 17 | # Compile the standalone application 18 | # 19 | SRC_FILES := $(call rwildcard,src,*.cpp) 20 | INCLUDES := $(P)/include src 21 | VPATH := $(P)/demo 22 | INTERNAL_LIBS := $(call internal-lib,$(PROJECT_NAME)) 23 | 24 | ################################################### 25 | # Generic Makefile rules 26 | # 27 | include $(M)/rules/Makefile 28 | 29 | $(INTERNAL_LIBS): -------------------------------------------------------------------------------- /src/utils/glob.cpp: -------------------------------------------------------------------------------- 1 | #include "glob.hpp" 2 | 3 | // clang-format off 4 | std::regex globToRegex(const std::string& glob) 5 | { 6 | std::string regex_pattern; 7 | regex_pattern.reserve(glob.size() * 2); 8 | 9 | regex_pattern += "^"; // Start of line 10 | 11 | for (size_t i = 0; i < glob.size(); ++i) 12 | { 13 | char c = glob[i]; 14 | switch (c) 15 | { 16 | case '*': 17 | regex_pattern += ".*"; // * correspond to any number of characters 18 | break; 19 | case '?': 20 | regex_pattern += "."; // ? corresponds to a single character 21 | break; 22 | case '.': case '+': case '(': case ')': case '{': case '}': 23 | case '|': case '^': case '$': case '[': case ']': case '\\': 24 | // Escape special characters in regex 25 | regex_pattern += '\\'; 26 | regex_pattern += c; 27 | break; 28 | default: 29 | regex_pattern += c; 30 | break; 31 | } 32 | } 33 | 34 | regex_pattern += "$"; // End of line 35 | return std::regex(regex_pattern); 36 | } 37 | // clang-format on 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 seb https://github.com/sebastiandev 4 | Copyright (c) 2022 Quentin Quadrat https://github.com/Lecrapouille 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | ################################################### 2 | # Location of the project directory and Makefiles 3 | # 4 | P := .. 5 | M := $(P)/.makefile 6 | 7 | ################################################### 8 | # Project definition 9 | # 10 | include $(P)/Makefile.common 11 | TARGET_NAME := $(PROJECT_NAME)-tests 12 | TARGET_DESCRIPTION := Unit tests for $(PROJECT_NAME) 13 | COMPILATION_MODE := test 14 | CXX_STANDARD := --std=c++17 15 | include $(M)/project/Makefile 16 | 17 | ################################################### 18 | # Compile the unit tests 19 | # 20 | SRC_FILES := $(call rwildcard,$(P)/src,*.cpp) 21 | SRC_FILES += $(call rwildcard,$(P)/tests,*.cpp) 22 | INCLUDES := $(P) $(P)/include $(P)/src 23 | VPATH := $(P) $(P)/src $(P)/src/utils 24 | DEFINES += -DPWD=\"$(shell pwd)\" 25 | DEFINES += -DHAVE_AES 26 | ifeq ($(ARCHI),Windows) 27 | LIB_FILES += $(P)/src/utils/dirent.c 28 | endif 29 | THIRD_PARTIES_LIBS := $(abspath $(THIRD_PARTIES_DIR)/minizip/build/libminizip.a) 30 | THIRD_PARTIES_LIBS += $(abspath $(THIRD_PARTIES_DIR)/minizip/build/libaes.a) 31 | THIRD_PARTIES_LIBS += $(abspath $(THIRD_PARTIES_DIR)/zlib-ng/build/libz.a) 32 | PKG_LIBS += gtest gmock 33 | USER_CXXFLAGS += -Wno-null-dereference 34 | 35 | ################################################### 36 | # Generic Makefile rules 37 | # 38 | include $(M)/rules/Makefile -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Tests CMake Configuration 3 | ############################################################################## 4 | 5 | # Needed for GoogleTest 6 | set(CMAKE_CXX_STANDARD 14) 7 | 8 | # Force Google Test to use shared CRT on Windows (/MD or /MDd) 9 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 10 | 11 | # Download and configure GoogleTest 12 | include(FetchContent) 13 | FetchContent_Declare( 14 | googletest 15 | GIT_REPOSITORY https://github.com/google/googletest.git 16 | GIT_TAG v1.14.0 17 | ) 18 | FetchContent_MakeAvailable(googletest) 19 | 20 | # Define the test sources 21 | file(GLOB TESTS_SOURCES "*.cpp") 22 | 23 | # Define the test executable 24 | add_executable(zipper-tests ${TESTS_SOURCES}) 25 | 26 | # Configure the include directories 27 | target_include_directories(zipper-tests PRIVATE 28 | ${CMAKE_SOURCE_DIR} 29 | ${CMAKE_SOURCE_DIR}/include 30 | ${CMAKE_SOURCE_DIR}/src 31 | ) 32 | 33 | # Define important macros 34 | target_compile_definitions(zipper-tests PRIVATE 35 | PWD="${CMAKE_CURRENT_SOURCE_DIR}" 36 | HAVE_AES 37 | ) 38 | 39 | # Link with the main library and Google Test 40 | target_link_libraries(zipper-tests PRIVATE 41 | zipper 42 | gtest 43 | gtest_main 44 | gmock 45 | ) 46 | 47 | # Add the test to the CTest list 48 | add_test(NAME zipper-tests COMMAND zipper-tests) -------------------------------------------------------------------------------- /src/utils/Timestamp.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | //----------------------------------------------------------------------------- 7 | 8 | #ifndef ZIPPER_UTILS_TIMESTAMP_HPP 9 | #define ZIPPER_UTILS_TIMESTAMP_HPP 10 | 11 | #if defined(_WIN32) 12 | # include 13 | #elif defined(__linux__) 14 | # include 15 | #endif 16 | 17 | #include 18 | #include 19 | 20 | namespace zipper 21 | { 22 | 23 | // ***************************************************************************** 24 | //! \brief Creates a timestap either from file or just current time If it fails 25 | //! to read the timestamp of a file, it set the time stamp to current time 26 | //! 27 | //! \warning It uses std::time to get current time, which is not standardized to 28 | //! be 1970-01-01.... However, it works on Windows and Unix 29 | //! https://stackoverflow.com/questions/6012663/get-unix-timestamp-with-c With 30 | //! C++20 this will be standardized 31 | // ***************************************************************************** 32 | struct Timestamp 33 | { 34 | Timestamp(); 35 | Timestamp(const std::string& filepath); 36 | 37 | tm timestamp; 38 | }; 39 | 40 | } // namespace zipper 41 | 42 | #endif // ZIPPER_UTILS_TIMESTAMP_HPP 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ################################################### 2 | # Location of the project directory and Makefiles 3 | # 4 | P := . 5 | M := $(P)/.makefile 6 | 7 | ################################################### 8 | # Project definition 9 | # 10 | include $(P)/Makefile.common 11 | TARGET_NAME := $(PROJECT_NAME) 12 | TARGET_DESCRIPTION := Zipper is a C++ library for creating and reading zip archives. 13 | CXX_STANDARD := --std=c++14 14 | include $(M)/project/Makefile 15 | 16 | ################################################### 17 | # Compile shared and static libraries 18 | # 19 | LIB_FILES := $(call rwildcard,src,*.cpp) 20 | ifeq ($(OS),Windows) 21 | LIB_FILES += src/utils/dirent.c 22 | endif 23 | INCLUDES := $(P)/include $(P)/src $(P) 24 | VPATH := $(P)/src $(P)/src/utils $(THIRD_PARTIES_DIR) 25 | DEFINES += -DHAVE_AES 26 | 27 | ################################################### 28 | # Third party libraries (compiled with make compile-external-libs) 29 | # 30 | THIRD_PARTIES_LIBS := $(abspath $(THIRD_PARTIES_DIR)/minizip/build/libminizip.a) 31 | THIRD_PARTIES_LIBS += $(abspath $(THIRD_PARTIES_DIR)/minizip/build/libaes.a) 32 | THIRD_PARTIES_LIBS += $(abspath $(THIRD_PARTIES_DIR)/zlib-ng/build/libz.a) 33 | 34 | ################################################### 35 | # Documentation 36 | # 37 | PATH_PROJECT_LOGO := $(PROJECT_DOC_DIR)/doxygen-logo.png 38 | 39 | ################################################### 40 | # Generic Makefile rules 41 | # 42 | include $(M)/rules/Makefile 43 | 44 | ################################################### 45 | # Extra rules 46 | # 47 | all:: demos 48 | 49 | .PHONY: demos 50 | demos: $(TARGET_STATIC_LIB_NAME) 51 | $(Q)$(MAKE) --no-print-directory --directory=doc/demos/Unzipper all 52 | $(Q)$(MAKE) --no-print-directory --directory=doc/demos/Zipper all 53 | 54 | clean:: 55 | $(Q)$(MAKE) --no-print-directory --directory=doc/demos/Unzipper clean 56 | $(Q)$(MAKE) --no-print-directory --directory=doc/demos/Zipper clean 57 | 58 | install:: 59 | $(Q)$(MAKE) --no-print-directory --directory=doc/demos/Unzipper install 60 | $(Q)$(MAKE) --no-print-directory --directory=doc/demos/Zipper install 61 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Version 3.0.2: 2 | * Use tags on third parties 3 | * Update MyMakefile, fixing issues when installing on OS 4 | 5 | Version 3.0.1: 6 | * Fix CMake find_package 7 | * Fix CMake using C+11 instead of C++14 8 | 9 | Version 3.0.0: 10 | * Add CMake to compile for Windows. 11 | * Fix memory corruptions found in minizip. 12 | * Fix compilation issue for minizip and Windows preventing usage of AES. 13 | * Fix regression in minizip when updated: timestamp for Linux. 14 | * Breaking API: 15 | - introduce dummy constructor and open() methods 16 | - replace bool by enum for extraction 17 | - reduce public method names 18 | - allows operators & and | for zip flags. 19 | * New API: 20 | - Add glob pattern (extraction, entries) 21 | - Add callback for making progressbar. 22 | * Better prevention against zip slip attack. 23 | * Cleaning C++ code. 24 | * Refresh unit tests. 25 | * Add clang-format and use C++ syntax convention for var and arguments. 26 | 27 | Version 2.2.0: 28 | * Optimized code by AI: same time for zipping/unzipping than Linux tool 29 | * Fix some issues with big names (entry, fake extraction below folder). 30 | * CI Ubuntu 22 and update MyMakefile. 31 | 32 | Version 2.1.1: 33 | * Compiled with MyMakefile-v2 34 | 35 | Version 2.1.0: 36 | * Update thridpart minizip-ng fixing issues to SHA1 0bb5afeb0d3f23149b086ccda7e4fee7d48f4fdf (2017). 37 | - Fix https://github.com/Lecrapouille/zipper/issues/8 38 | - Fix https://github.com/Lecrapouille/zipper/issues/9 39 | * Currently I cannot track more recent SHA1 because the API is changing. 40 | 41 | Version 2.0.0: 42 | * Forked from https://github.com/sebastiandev/zipper v2.x.y branch. 43 | * Replace CMake by Makefile to simplify compilation build (lot of issues come from CMake). 44 | * Replace Catch by Google test. Add lot of non regression tests. 45 | * Fix all issues reported by users. 46 | * Use by default EAS. 47 | * Prevent against zip slip: this has broken the API from initial project. 48 | * Track the HEAD of zlib-ng thirdpart and 2015 SHA1 of minizip-ng.. 49 | 50 | Previous version: 51 | * Original project https://github.com/sebastiandev/zipper (master branch) 52 | -------------------------------------------------------------------------------- /src/utils/OS.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | //----------------------------------------------------------------------------- 7 | 8 | #ifndef ZIPPER_UTILS_OS_HPP 9 | #define ZIPPER_UTILS_OS_HPP 10 | 11 | extern "C" 12 | { 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define WINDOWS_DIRECTORY_SEPARATOR '\\' 22 | #define UNIX_DIRECTORY_SEPARATOR '/' 23 | #if defined(_WIN32) 24 | # define DIRECTORY_SEPARATOR WINDOWS_DIRECTORY_SEPARATOR 25 | #else 26 | # define DIRECTORY_SEPARATOR UNIX_DIRECTORY_SEPARATOR 27 | #endif 28 | 29 | #if defined(_WIN32) 30 | # include 31 | # include 32 | typedef struct _stat STAT; 33 | # define stat _stat 34 | # define S_IFREG _S_IFREG 35 | # define S_IFDIR _S_IFDIR 36 | # define access _access 37 | # define mkdir _mkdir 38 | # define rmdir _rmdir 39 | #else 40 | # include 41 | # include 42 | # include 43 | # include 44 | typedef struct stat STAT; 45 | #endif 46 | 47 | #if defined(_WIN32) 48 | # define USEWIN32IOAPI 49 | # include "external/minizip/ioapi.h" 50 | # include "external/minizip/iowin32.h" 51 | #endif 52 | 53 | } // extern C 54 | 55 | #if defined(_WIN64) && (!defined(__APPLE__)) 56 | # ifndef __USE_FILE_OFFSET64 57 | # define __USE_FILE_OFFSET64 58 | # endif 59 | # ifndef __USE_LARGEFILE64 60 | # define __USE_LARGEFILE64 61 | # endif 62 | # ifndef _LARGEFILE64_SOURCE 63 | # define _LARGEFILE64_SOURCE 64 | # endif 65 | # ifndef _FILE_OFFSET_BIT 66 | # define _FILE_OFFSET_BIT 64 67 | # endif 68 | #endif 69 | 70 | #if defined(_WIN32) 71 | # define OS_STRERROR ::strerror // FIXME win_strerror 72 | # define OS_MKDIR(d, v) ::_mkdir(d) 73 | # define OS_CHDIR(d) ::_chdir(d) 74 | # define OS_GETCWD(b, s) ::_getcwd(b, int(s)) 75 | # define OS_UNLINK(f) ::_unlink(f) 76 | # define OS_RMDIR(d) ::_rmdir(d) 77 | # define OS_REMOVE(f) ::remove(f) 78 | # define OS_RENAME(o, n) ::rename(o, n) 79 | #else 80 | # define OS_STRERROR ::strerror 81 | # define OS_MKDIR(d, v) ::mkdir(d, v) 82 | # define OS_CHDIR(d) ::chdir(d) 83 | # define OS_GETCWD(b, s) ::getcwd(b, s) 84 | # define OS_UNLINK(f) ::unlink(f) 85 | # define OS_RMDIR(d) ::rmdir(d) 86 | # define OS_REMOVE(f) ::remove(f) 87 | # define OS_RENAME(o, n) ::rename(o, n) 88 | #endif 89 | 90 | #endif // ZIPPER_UTILS_OS_HPP 91 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Google 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Empty 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: Yes 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BraceWrapping: 24 | AfterClass: true 25 | AfterControlStatement: true 26 | AfterEnum: true 27 | AfterFunction: true 28 | AfterNamespace: false 29 | AfterObjCDeclaration: true 30 | AfterStruct: true 31 | AfterUnion: true 32 | BeforeCatch: true 33 | BeforeElse: true 34 | IndentBraces: false 35 | BreakBeforeBinaryOperators: None 36 | BreakBeforeBraces: Allman 37 | BreakBeforeInheritanceComma: false 38 | BreakInheritanceList: BeforeColon 39 | BreakBeforeTernaryOperators: true 40 | BreakConstructorInitializersBeforeComma: false 41 | BreakConstructorInitializers: BeforeColon 42 | BreakAfterJavaFieldAnnotations: false 43 | BreakStringLiterals: true 44 | ColumnLimit: 80 45 | CommentPragmas: '^ IWYU pragma:' 46 | CompactNamespaces: false 47 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 48 | ConstructorInitializerIndentWidth: 4 49 | ContinuationIndentWidth: 4 50 | Cpp11BracedListStyle: false 51 | DerivePointerAlignment: false 52 | DisableFormat: false 53 | ExperimentalAutoDetectBinPacking: false 54 | FixNamespaceComments: true 55 | ForEachMacros: 56 | - foreach 57 | - Q_FOREACH 58 | - BOOST_FOREACH 59 | IncludeBlocks: Preserve 60 | IncludeCategories: 61 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 62 | Priority: 2 63 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 64 | Priority: 3 65 | - Regex: '.*' 66 | Priority: 1 67 | IncludeIsMainRegex: '(Test)?$' 68 | IndentCaseLabels: true 69 | IndentPPDirectives: AfterHash 70 | IndentWidth: 4 71 | IndentWrappedFunctionNames: false 72 | JavaScriptQuotes: Leave 73 | JavaScriptWrapImports: true 74 | KeepEmptyLinesAtTheStartOfBlocks: false 75 | MacroBlockBegin: '' 76 | MacroBlockEnd: '' 77 | MaxEmptyLinesToKeep: 1 78 | NamespaceIndentation: None 79 | ObjCBinPackProtocolList: Auto 80 | ObjCBlockIndentWidth: 2 81 | ObjCSpaceAfterProperty: false 82 | ObjCSpaceBeforeProtocolList: true 83 | PenaltyBreakAssignment: 2 84 | PenaltyBreakBeforeFirstCallParameter: 19 85 | PenaltyBreakComment: 300 86 | PenaltyBreakFirstLessLess: 120 87 | PenaltyBreakString: 1000 88 | PenaltyBreakTemplateDeclaration: 10 89 | PenaltyExcessCharacter: 1000000 90 | PenaltyReturnTypeOnItsOwnLine: 60 91 | PointerAlignment: Left 92 | ReflowComments: true 93 | SortIncludes: true 94 | SortUsingDeclarations: true 95 | SpaceAfterCStyleCast: false 96 | SpaceAfterTemplateKeyword: true 97 | SpaceBeforeAssignmentOperators: true 98 | SpaceBeforeCpp11BracedList: false 99 | SpaceBeforeCtorInitializerColon: true 100 | SpaceBeforeInheritanceColon: false 101 | EmptyLineBeforeAccessModifier: Always 102 | EmptyLineAfterAccessModifier: Always 103 | SpaceBeforeParens: ControlStatements 104 | SpaceBeforeRangeBasedForLoopColon: true 105 | SpaceInEmptyParentheses: false 106 | SpacesBeforeTrailingComments: 1 107 | SpacesInAngles: false 108 | SpacesInContainerLiterals: false 109 | SpacesInCStyleCastParentheses: false 110 | SpacesInParentheses: false 111 | SpacesInSquareBrackets: false 112 | Standard: Cpp11 113 | TabWidth: 4 114 | UseTab: Never -------------------------------------------------------------------------------- /src/utils/Timestamp.cpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | //----------------------------------------------------------------------------- 7 | 8 | #include "Timestamp.hpp" 9 | #include 10 | #include 11 | #if !defined(_WIN32) 12 | # include 13 | #endif 14 | 15 | using namespace zipper; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // Helper function for safe localtime access across platforms 19 | // ----------------------------------------------------------------------------- 20 | static inline tm* safe_localtime(const time_t* time, tm* result) 21 | { 22 | #if defined(_WIN32) 23 | // Use localtime_s on Windows which is thread-safe 24 | if (localtime_s(result, time) != 0) 25 | { 26 | return nullptr; 27 | } 28 | return result; 29 | #else 30 | // On Unix-like systems 31 | # if defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 1 || \ 32 | defined(_XOPEN_SOURCE) || defined(_BSD_SOURCE) || \ 33 | defined(_SVID_SOURCE) || defined(_POSIX_SOURCE) 34 | // Use localtime_r if available (thread-safe) 35 | return localtime_r(time, result); 36 | # else 37 | // Fallback to localtime on platforms without localtime_r 38 | // Note: This is not thread-safe 39 | const tm* tmp = std::localtime(time); 40 | if (tmp == nullptr) 41 | { 42 | return nullptr; 43 | } 44 | *result = *tmp; 45 | return result; 46 | # endif 47 | #endif 48 | } 49 | 50 | // ----------------------------------------------------------------------------- 51 | Timestamp::Timestamp() 52 | { 53 | std::time_t now = std::time(nullptr); 54 | tm temp_tm; 55 | if (safe_localtime(&now, &temp_tm) != nullptr) 56 | { 57 | timestamp = temp_tm; 58 | } 59 | } 60 | 61 | // ----------------------------------------------------------------------------- 62 | Timestamp::Timestamp(const std::string& filepath) 63 | { 64 | // Set default 65 | std::time_t now = std::time(nullptr); 66 | tm temp_tm; 67 | if (safe_localtime(&now, &temp_tm) != nullptr) 68 | { 69 | timestamp = temp_tm; 70 | } 71 | 72 | #if defined(_WIN32) 73 | 74 | // Implementation based on Ian Boyd's 75 | // https://stackoverflow.com/questions/20370920/convert-current-time-from-windows-to-unix-timestamp-in-c-or-c 76 | HANDLE hFile1; 77 | FILETIME filetime; 78 | hFile1 = CreateFile(filepath.c_str(), 79 | GENERIC_READ, 80 | FILE_SHARE_READ, 81 | nullptr, 82 | OPEN_EXISTING, 83 | FILE_ATTRIBUTE_NORMAL, 84 | nullptr); 85 | 86 | if (hFile1 == INVALID_HANDLE_VALUE) 87 | { 88 | return; 89 | } 90 | 91 | if (!GetFileTime(hFile1, &filetime, nullptr, nullptr)) 92 | { 93 | CloseHandle(hFile1); 94 | return; 95 | } 96 | const int64_t UNIX_TIME_START = 97 | 0x019DB1DED53E8000; // January 1, 1970 (start of Unix epoch) in "ticks" 98 | const int64_t TICKS_PER_SECOND = 10000000; // a tick is 100ns 99 | 100 | // Copy the low and high parts of FILETIME into a LARGE_INTEGER 101 | // This is so we can access the full 64-bits as an Int64 without causing an 102 | // alignment fault. 103 | LARGE_INTEGER li; 104 | li.LowPart = filetime.dwLowDateTime; 105 | li.HighPart = filetime.dwHighDateTime; 106 | 107 | // Convert ticks since 1/1/1970 into seconds 108 | time_t time_s = (li.QuadPart - UNIX_TIME_START) / TICKS_PER_SECOND; 109 | 110 | safe_localtime(&time_s, ×tamp); 111 | CloseHandle(hFile1); 112 | 113 | #else // !_WIN32 114 | 115 | struct stat buf; 116 | if (stat(filepath.c_str(), &buf) != 0) 117 | { 118 | return; 119 | } 120 | 121 | # if defined(__APPLE__) 122 | auto timet = static_cast(buf.st_mtimespec.tv_sec); 123 | # else 124 | auto timet = buf.st_mtim.tv_sec; 125 | # endif 126 | 127 | safe_localtime(&timet, ×tamp); 128 | 129 | #endif // _WIN32 130 | } 131 | -------------------------------------------------------------------------------- /doc/demos/Zipper/src/demo_zip.cpp: -------------------------------------------------------------------------------- 1 | #include "Zipper/Zipper.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #ifdef WIN32 8 | # include 9 | #else 10 | # include 11 | # include 12 | #endif 13 | 14 | #define PROGRESS_BAR_WIDTH 50 15 | 16 | // ---------------------------------------------------------------------------- 17 | //! \brief Display the usage of the program 18 | //! \param[in] argv0 Name of the program 19 | // ---------------------------------------------------------------------------- 20 | static void usage(const char* argv0) 21 | { 22 | std::cout << "Usage:\n " << argv0 23 | << " [[-p] [-o path/to/output.zip] \n\n" 24 | << "Where:\n" 25 | << " -p with AES password (from stdin)\n" 26 | << " -o path to the zip file to create (default: ./output.zip)\n" 27 | << std::endl; 28 | } 29 | 30 | // ---------------------------------------------------------------------------- 31 | //! \brief Quick and dirty parser the command-line option. 32 | // ---------------------------------------------------------------------------- 33 | static std::string 34 | cli(int argc, char* const argv[], const std::string& short_option) 35 | { 36 | for (int i = 1; i < argc; ++i) 37 | { 38 | std::string arg(argv[i]); 39 | if (arg == short_option) 40 | { 41 | if (i + 1 < argc) 42 | return argv[i + 1]; 43 | return argv[i]; 44 | } 45 | } 46 | return {}; 47 | } 48 | 49 | // ---------------------------------------------------------------------------- 50 | //! \brief Display a progress bar. 51 | //! \param[in] progress Progress information. 52 | // ---------------------------------------------------------------------------- 53 | static void displayProgress(const zipper::Zipper::Progress& progress) 54 | { 55 | // Compute the global percentage 56 | float percent = 0.0f; 57 | if (progress.total_bytes > 0) 58 | { 59 | percent = float(progress.bytes_processed) / 60 | float(progress.total_bytes) * 100.0f; 61 | } 62 | 63 | // Compute the number of characters to display 64 | size_t filled = size_t((percent / 100.0f) * PROGRESS_BAR_WIDTH); 65 | 66 | // Display the progress bar 67 | std::cout << "\r["; 68 | for (size_t i = 0; i < PROGRESS_BAR_WIDTH; ++i) 69 | { 70 | if (i < filled) 71 | std::cout << "="; 72 | else 73 | std::cout << " "; 74 | } 75 | std::cout << "] " << std::fixed << std::setprecision(1) << percent << "% "; 76 | 77 | // Display the current file and the number of files 78 | std::cout << "(" << progress.files_compressed << "/" << progress.total_files 79 | << ") "; 80 | std::cout << "'" << progress.current_file << "'"; 81 | 82 | // Clean the line 83 | std::cout << std::string(20, ' ') << "\r" << std::flush; 84 | 85 | // If finished, go to the next line 86 | if (progress.status != zipper::Zipper::Progress::Status::InProgress) 87 | { 88 | std::cout << std::endl; 89 | } 90 | } 91 | 92 | // ---------------------------------------------------------------------------- 93 | //! \brief Compress a given folder into a zip file with an optional password 94 | // ---------------------------------------------------------------------------- 95 | int main(int argc, char* argv[]) 96 | { 97 | // Analyze the command-line 98 | if ((argc < 2) || (cli(argc, argv, "-h") != "")) 99 | { 100 | usage(argv[0]); 101 | return EXIT_FAILURE; 102 | } 103 | 104 | std::string folder_path(argv[argc - 1]); 105 | std::string zip_file(cli(argc, argv, "-o")); 106 | bool with_password(cli(argc, argv, "-p") != ""); 107 | bool recursive(cli(argc, argv, "-r") != ""); 108 | 109 | // Default value for the zip file if not specified 110 | if (zip_file.empty()) 111 | { 112 | zip_file = "output.zip"; 113 | } 114 | 115 | // Summary of the analyzed options 116 | std::cout << "folder to compress: " << folder_path << "\n" 117 | << "zip file: " << zip_file << "\n" 118 | << "with password: " << with_password << "\n" 119 | << "recursive compression: " << recursive << "\n" 120 | << std::endl; 121 | 122 | // Check if the last argument is a valid path 123 | if (folder_path.empty() || folder_path[0] == '-') 124 | { 125 | std::cerr 126 | << "CLI error: The last argument must be a folder or file path" 127 | << std::endl; 128 | return EXIT_FAILURE; 129 | } 130 | 131 | // Read the password 132 | std::string password; 133 | if (with_password) 134 | { 135 | std::cout << "\nEnter your password: " << std::endl; 136 | std::cin >> password; 137 | } 138 | 139 | // libZipper functions 140 | try 141 | { 142 | zipper::Zipper zipper(zip_file, password, zipper::Zipper::Overwrite); 143 | 144 | // Set the progress callback 145 | zipper.setProgressCallback(displayProgress); 146 | 147 | if (!zipper.add(folder_path, zipper::Zipper::Better)) 148 | { 149 | std::error_code err = zipper.error(); 150 | std::cerr << "Compression failed for '" << folder_path << "' to '" 151 | << zip_file << "' Reason: '" << err.message() << "'" 152 | << std::endl; 153 | return EXIT_FAILURE; 154 | } 155 | 156 | zipper.close(); 157 | } 158 | catch (std::runtime_error const& e) 159 | { 160 | std::cerr << "Compression failed for '" << folder_path 161 | << "' Exception: '" << e.what() << "'" << std::endl; 162 | return EXIT_FAILURE; 163 | } 164 | 165 | std::cout << "[ok]" << std::endl; 166 | return EXIT_SUCCESS; 167 | } -------------------------------------------------------------------------------- /tests/TestHelper.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UNIT_TESTS_HELPER_HPP 2 | #define UNIT_TESTS_HELPER_HPP 3 | 4 | #include "utils/Path.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | //============================================================================= 12 | // Helper functions for tests 13 | //============================================================================= 14 | namespace helper 15 | { 16 | 17 | /** 18 | * @brief Reads and returns the content of a file. 19 | * @param[in] p_file Path to the file to read. 20 | * @return The content of the file as a string. 21 | */ 22 | inline std::string readFileContent(const std::string& p_file) 23 | { 24 | std::ifstream ifs(p_file); 25 | std::string str((std::istreambuf_iterator(ifs)), 26 | std::istreambuf_iterator()); 27 | return str.c_str(); 28 | } 29 | 30 | /** 31 | * @brief Checks if a file exists and has the expected content. 32 | * @param[in] p_file Path to the file to check. 33 | * @param[in] p_content Content to check in the file. 34 | * @return true if the file exists and has the expected content, false 35 | * otherwise. 36 | */ 37 | inline bool checkFileExists(const std::string& p_file, 38 | const std::string& p_content) 39 | { 40 | return zipper::Path::exist(p_file) && zipper::Path::isFile(p_file) && 41 | readFileContent(p_file) == p_content; 42 | } 43 | 44 | inline bool checkFileExists(const std::string& p_file) 45 | { 46 | return zipper::Path::exist(p_file) && zipper::Path::isFile(p_file); 47 | } 48 | 49 | /** 50 | * @brief Checks if a file does not exist. 51 | * @param[in] p_file Path to the file to check. 52 | * @return true if the file does not exist, false otherwise. 53 | */ 54 | inline bool checkFileDoesNotExist(const std::string& p_file) 55 | { 56 | return !(zipper::Path::exist(p_file) && zipper::Path::isFile(p_file)); 57 | } 58 | 59 | /** 60 | * @brief Checks if a directory exists. 61 | * @param[in] p_dir Path to the directory to check. 62 | * @return true if the directory exists, false otherwise. 63 | */ 64 | inline bool checkDirExists(const std::string& p_dir) 65 | { 66 | return zipper::Path::exist(p_dir) && zipper::Path::isDir(p_dir); 67 | } 68 | 69 | /** 70 | * @brief Checks if a directory does not exist. 71 | * @param[in] p_dir Path to the directory to check. 72 | * @return true if the directory does not exist, false otherwise. 73 | */ 74 | inline bool checkDirDoesNotExist(const std::string& p_dir) 75 | { 76 | return !(zipper::Path::exist(p_dir) && zipper::Path::isDir(p_dir)); 77 | } 78 | 79 | /** 80 | * @brief Creates a file with content. 81 | * @param[in] p_file Path to the file to create. 82 | * @param[in] p_content Content to write in the file. 83 | * @return true if successful, false otherwise. 84 | */ 85 | inline bool createFile(const std::string& p_file, const std::string& p_content) 86 | { 87 | zipper::Path::remove(p_file); 88 | 89 | std::ofstream ofs(p_file); 90 | ofs << p_content; 91 | ofs.flush(); 92 | ofs.close(); 93 | 94 | return checkFileExists(p_file, p_content); 95 | } 96 | 97 | /** 98 | * @brief Removes a file or directory. 99 | * @param[in] p_file Path to the file or directory to remove. 100 | * @return true if successful, false otherwise. 101 | */ 102 | inline bool removeFileOrDir(const std::string& p_file) 103 | { 104 | if (zipper::Path::isFile(p_file)) 105 | { 106 | zipper::Path::remove(p_file); 107 | return checkFileDoesNotExist(p_file); 108 | } 109 | else if (zipper::Path::isDir(p_file)) 110 | { 111 | return zipper::Path::remove(p_file); 112 | return (!checkDirExists(p_file)); 113 | } 114 | return true; 115 | } 116 | 117 | /** 118 | * @brief Creates a directory. 119 | * @param[in] p_dir Path to the directory to create. 120 | * @return true if successful, false otherwise. 121 | */ 122 | inline bool createDir(const std::string& p_dir) 123 | { 124 | return removeFileOrDir(p_dir) && zipper::Path::createDir(p_dir) && 125 | checkDirExists(p_dir); 126 | } 127 | 128 | /** 129 | * @brief Checks if a directory is empty. 130 | * @param[in] p_dir Path to the directory to check. 131 | * @return true if the directory is empty, false otherwise. 132 | */ 133 | inline bool isDirEmpty(const std::string& p_dir) 134 | { 135 | if (!zipper::Path::isDir(p_dir)) 136 | return false; 137 | 138 | std::vector entries = zipper::Path::filesFromDir(p_dir, false); 139 | return entries.empty(); 140 | } 141 | 142 | /** 143 | * @brief Creates a file with content and adds it to the zipper. 144 | * @param[in] p_zipper The zipper instance. 145 | * @param[in] p_file_path Path of the temporary file to create. 146 | * @param[in] p_content Content to write in the file. 147 | * @param[in] p_entry_path Path of the entry in the zip archive. 148 | * @return true if successful, false otherwise. 149 | */ 150 | inline bool zipAddFile(zipper::Zipper& p_zipper, 151 | const char* p_file_path, // FIXME useless: to be removed 152 | const char* p_content, 153 | const char* p_entry_path) 154 | { 155 | if (!helper::createFile(p_file_path, p_content)) 156 | { 157 | return false; 158 | } 159 | 160 | std::ifstream ifs(p_file_path); 161 | bool res = p_zipper.add(ifs, p_entry_path, zipper::Zipper::SaveHierarchy); 162 | ifs.close(); 163 | 164 | helper::removeFileOrDir(p_file_path); 165 | 166 | return res; 167 | } 168 | 169 | //----------------------------------------------------------------------------- 170 | //! \brief Convert an integer to a base-36 string (digits 0-9, letters A-Z). 171 | //! \param[in] value The integer value to convert (must be >= 0). 172 | //! \return The base-36 string representation. 173 | //----------------------------------------------------------------------------- 174 | inline std::string intToBase36(size_t value) 175 | { 176 | static const char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 177 | if (value < 36) 178 | return std::string(1, digits[value]); 179 | 180 | // On commence à 2 digits après 35 ("00"), puis 3, etc. 181 | size_t n = value - 36; 182 | size_t width = 2; 183 | size_t max = 36 * 36; 184 | while (n >= max) 185 | { 186 | n -= max; 187 | ++width; 188 | max *= 36; 189 | } 190 | 191 | std::string result(width, '0'); 192 | size_t i = width; 193 | while (i-- > 0) 194 | { 195 | result[i] = digits[n % 36]; 196 | n /= 36; 197 | } 198 | return result; 199 | } 200 | 201 | } // namespace helper 202 | 203 | #endif // UNIT_TESTS_HELPER_HPP -------------------------------------------------------------------------------- /doc/demos/Unzipper/src/demo_unzip.cpp: -------------------------------------------------------------------------------- 1 | #include "Zipper/Unzipper.hpp" 2 | #include 3 | #include 4 | #include 5 | 6 | #ifdef WIN32 7 | # include 8 | #else 9 | # include 10 | # include 11 | #endif 12 | 13 | #define DEFAULT_MAX_UNCOMPRESSED_SIZE_GB 6 14 | #define TO_GB(bytes) (bytes * 1024 * 1024 * 1024) 15 | #define PROGRESS_BAR_WIDTH 50 16 | 17 | // ---------------------------------------------------------------------------- 18 | //! \brief Display a progress bar. 19 | //! \param[in] progress Progress information. 20 | // ---------------------------------------------------------------------------- 21 | static void displayProgress(const zipper::Unzipper::Progress& progress) 22 | { 23 | // Compute the global percentage 24 | float percent = 0.0f; 25 | if (progress.total_bytes > 0) 26 | { 27 | percent = 28 | float(progress.bytes_read) / float(progress.total_bytes) * 100.0f; 29 | } 30 | 31 | // Compute the number of characters to display 32 | size_t filled = size_t((percent / 100.0f) * PROGRESS_BAR_WIDTH); 33 | 34 | // Display the progress bar 35 | std::cout << "\r["; 36 | for (size_t i = 0; i < PROGRESS_BAR_WIDTH; ++i) 37 | { 38 | if (i < filled) 39 | std::cout << "="; 40 | else 41 | std::cout << " "; 42 | } 43 | std::cout << "] " << std::fixed << std::setprecision(1) << percent << "% "; 44 | 45 | // Display the current file and the number of files 46 | std::cout << "(" << progress.files_extracted << "/" << progress.total_files 47 | << ") "; 48 | std::cout << "'" << progress.current_file << "'"; 49 | 50 | // Clean the line 51 | std::cout << std::string(20, ' ') << "\r" << std::flush; 52 | 53 | // If finished, go to the next line 54 | if (progress.status != zipper::Unzipper::Progress::Status::InProgress) 55 | { 56 | std::cout << std::endl; 57 | } 58 | } 59 | 60 | // ---------------------------------------------------------------------------- 61 | static void usage(const char* argv0) 62 | { 63 | std::cout << "Usage:\n " << argv0 64 | << " [[-p] [-f] [-o path] [-m max_size] \n\n" 65 | << "Where:\n" 66 | << " -p with AES password (from stdin)\n" 67 | << " -o path to extract (default: .)\n" 68 | << " -f force smashing files\n" 69 | << " -m max uncompressed size in Giga bytes (default: " 70 | << DEFAULT_MAX_UNCOMPRESSED_SIZE_GB << ")\n" 71 | << std::endl; 72 | } 73 | 74 | // ---------------------------------------------------------------------------- 75 | //! \brief Quick and dirty parser the command-line option. 76 | // ---------------------------------------------------------------------------- 77 | static std::string 78 | cli(int argc, char* const argv[], const std::string& short_option) 79 | { 80 | for (int i = 1; i < argc; ++i) 81 | { 82 | std::string arg(argv[i]); 83 | if (arg == short_option) 84 | { 85 | if (i + 1 < argc) 86 | return argv[i + 1]; 87 | return argv[i]; 88 | } 89 | } 90 | return {}; 91 | } 92 | 93 | // ---------------------------------------------------------------------------- 94 | //! \brief Extract a given zip file to an optional destination with an optional 95 | //! password. 96 | // ---------------------------------------------------------------------------- 97 | int main(int argc, char* argv[]) 98 | { 99 | // Parse the command line 100 | if ((argc == 0) || (cli(argc, argv, "-h") != "")) 101 | { 102 | usage(argv[0]); 103 | return EXIT_FAILURE; 104 | } 105 | 106 | std::string zip_file(argv[argc - 1]); 107 | std::string extraction_path(cli(argc, argv, "-o")); 108 | bool with_password(cli(argc, argv, "-p") != ""); 109 | bool force(cli(argc, argv, "-f") != ""); 110 | std::string max_size_str = cli(argc, argv, "-m"); 111 | size_t max_uncompressed_size = DEFAULT_MAX_UNCOMPRESSED_SIZE_GB; 112 | if (!max_size_str.empty()) 113 | { 114 | try 115 | { 116 | max_uncompressed_size = std::stoull(max_size_str); 117 | } 118 | catch (const std::exception& e) 119 | { 120 | std::cerr << "Invalid value for -m (max uncompressed size): '" 121 | << max_size_str << "'\n"; 122 | return EXIT_FAILURE; 123 | } 124 | } 125 | 126 | // Resume parsed options 127 | std::cout << "zip file: " << zip_file << "\n" 128 | << "extraction path: " << extraction_path << "\n" 129 | << "with password: " << with_password << "\n" 130 | << "force smashing files: " << force << "\n" 131 | << "max uncompressed size: " << max_uncompressed_size 132 | << " Giga bytes\n" 133 | << std::endl; 134 | 135 | // Check against zip file extension 136 | if (zip_file.substr(zip_file.find_last_of(".") + 1) != "zip") 137 | { 138 | std::cerr 139 | << "CLI error: Expected zip file on the last argument position" 140 | << std::endl; 141 | return EXIT_FAILURE; 142 | } 143 | 144 | // Read the password 145 | std::string password; 146 | if (with_password) 147 | { 148 | std::cout << "\nType your password: " << std::endl; 149 | std::cin >> password; 150 | } 151 | 152 | // libZipper functions 153 | try 154 | { 155 | zipper::Unzipper unzipper(zip_file, password); 156 | 157 | if (unzipper.sizeOnDisk() > TO_GB(max_uncompressed_size)) 158 | { 159 | std::cerr 160 | << "Zip file uncompressed size exceeds the allowed limit (" 161 | << max_uncompressed_size 162 | << " GB). Use -m to set a different limit." << std::endl; 163 | return EXIT_FAILURE; 164 | } 165 | 166 | unzipper.setProgressCallback(displayProgress); 167 | 168 | if (!unzipper.extractAll( 169 | extraction_path, 170 | force ? zipper::Unzipper::OverwriteMode::Overwrite 171 | : zipper::Unzipper::OverwriteMode::DoNotOverwrite)) 172 | { 173 | std::error_code err = unzipper.error(); 174 | std::cerr << "Failed unzipping '" << zip_file << "' to '" 175 | << (extraction_path == "" ? "." : extraction_path) 176 | << "' Reason was: '" << err.message() << "'" << std::endl; 177 | return EXIT_FAILURE; 178 | } 179 | unzipper.close(); 180 | } 181 | catch (std::runtime_error const& e) 182 | { 183 | std::cerr << "Failed unzipping '" << zip_file << "' Exception was: '" 184 | << e.what() << "'" << std::endl; 185 | return EXIT_FAILURE; 186 | } 187 | 188 | std::cout << "[ok]" << std::endl; 189 | return EXIT_SUCCESS; 190 | } 191 | -------------------------------------------------------------------------------- /src/utils/dirent.h: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | //----------------------------------------------------------------------------- 7 | /* ///////////////////////////////////////////////////////////////////////////// 8 | * File: dirent.h 9 | * 10 | * Purpose: Declaration of the opendir() API functions and types for the 11 | * Win32 platform. 12 | * 13 | * Created 19th October 2002 14 | * Updated: 10th January 2005 15 | * 16 | * Home: http://synesis.com.au/software/ 17 | * 18 | * Copyright 2002-2005, Matthew Wilson and Synesis Software 19 | * All rights reserved. 20 | * 21 | * Redistribution and use in source and binary forms, with or without 22 | * modification, are permitted provided that the following conditions are met: 23 | * 24 | * - Redistributions of source code must retain the above copyright notice, this 25 | * list of conditions and the following disclaimer. 26 | * - Redistributions in binary form must reproduce the above copyright notice, 27 | * this list of conditions and the following disclaimer in the documentation 28 | * and/or other materials provided with the distribution. 29 | * - Neither the names of Matthew Wilson and Synesis Software nor the names of 30 | * any contributors may be used to endorse or promote products derived from 31 | * this software without specific prior written permission. 32 | * 33 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 34 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 35 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 36 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 37 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 38 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 39 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 40 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 41 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 42 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 43 | * POSSIBILITY OF SUCH DAMAGE. 44 | * 45 | * ////////////////////////////////////////////////////////////////////////// */ 46 | 47 | /** \file dirent.h 48 | * 49 | * Contains the declarations for the opendir()/readdir() API. 50 | */ 51 | 52 | #ifndef SYNSOFT_UNIXEM_INCL_H_DIRENT 53 | # define SYNSOFT_UNIXEM_INCL_H_DIRENT 54 | 55 | # ifndef _SYNSOFT_DOCUMENTATION_SKIP_SECTION 56 | # define SYNSOFT_UNIXEM_VER_H_DIRENT_MAJOR 2 57 | # define SYNSOFT_UNIXEM_VER_H_DIRENT_MINOR 1 58 | # define SYNSOFT_UNIXEM_VER_H_DIRENT_REVISION 1 59 | # define SYNSOFT_UNIXEM_VER_H_DIRENT_EDIT 19 60 | # endif /* !_SYNSOFT_DOCUMENTATION_SKIP_SECTION */ 61 | 62 | /* ////////////////////////////////////////////////////////////////////////// */ 63 | 64 | /** \weakgroup unixem Synesis Software UNIX Emulation for Win32 65 | * \brief The UNIX emulation library 66 | */ 67 | 68 | /** \weakgroup unixem_dirent opendir()/readdir() API 69 | * \ingroup UNIXem unixem 70 | * \brief This API provides facilities for enumerating the contents of 71 | * directories 72 | * @{ 73 | */ 74 | 75 | /* ////////////////////////////////////////////////////////////////////////// */ 76 | 77 | # ifndef _WIN32 78 | # error This file is only currently defined for compilation on Win32 systems 79 | # endif /* _WIN32 */ 80 | 81 | /* ///////////////////////////////////////////////////////////////////////////// 82 | * Constants and definitions 83 | */ 84 | 85 | # ifndef NAME_MAX 86 | # define NAME_MAX \ 87 | (260) /*!< \brief The maximum number of characters (including null \ 88 | terminator) in a directory entry name */ 89 | # endif /* !NAME_MAX */ 90 | 91 | /* ///////////////////////////////////////////////////////////////////////////// 92 | * Typedefs 93 | */ 94 | 95 | typedef struct dirent_dir DIR; /*!< \brief dirent_dir is defined internally */ 96 | 97 | /** \brief Results structure for readdir() 98 | */ 99 | struct dirent 100 | { 101 | char d_name[NAME_MAX + 1]; /*!< file name (null-terminated) */ 102 | }; 103 | 104 | /* ///////////////////////////////////////////////////////////////////////////// 105 | * API functions 106 | */ 107 | 108 | # ifdef __cplusplus 109 | extern "C" { 110 | # endif /* __cplusplus */ 111 | 112 | /** \brief Returns a pointer to the next directory entry. 113 | * 114 | * This function opens the directory named by filename, and returns a 115 | * directory to be used to in subsequent operations. NULL is returned 116 | * if name cannot be accessed, or if resources cannot be acquired to 117 | * process the request. 118 | * 119 | * \param name The name of the directory to search 120 | * \return The directory handle from which the entries are read or NULL 121 | */ 122 | DIR* opendir(const char* name); 123 | 124 | /** \brief Closes a directory handle 125 | * 126 | * This function closes a directory handle that was opened with opendir() 127 | * and releases any resources associated with that directory handle. 128 | * 129 | * \param dir The directory handle from which the entries are read 130 | * \return 0 on success, or -1 to indicate error. 131 | */ 132 | int closedir(DIR* dir); 133 | 134 | /** \brief Resets a directory search position 135 | * 136 | * This function resets the position of the named directory handle to 137 | * the beginning of the directory. 138 | * 139 | * \param dir The directory handle whose position should be reset 140 | */ 141 | void rewinddir(DIR* dir); 142 | 143 | /** \brief Returns a pointer to the next directory entry. 144 | * 145 | * This function returns a pointer to the next directory entry, or NULL upon 146 | * reaching the end of the directory or detecting an invalid seekdir() operation 147 | * 148 | * \param dir The directory handle from which the entries are read 149 | * \return A dirent structure or NULL 150 | */ 151 | struct dirent* readdir(DIR* dir); 152 | 153 | /** \brief Returns a string describing the error code 154 | * 155 | * This function returns a string describing the error code passed in the 156 | * errnum argument. 157 | * 158 | * \param errnum The error code to get the description for 159 | * \return A string describing the error code 160 | */ 161 | const char* win_strerror(int errnum); 162 | 163 | # ifdef __cplusplus 164 | } 165 | # endif /* __cplusplus */ 166 | 167 | /* ////////////////////////////////////////////////////////////////////////// */ 168 | 169 | /** @} // end of group unixem_dirent */ 170 | 171 | /* ////////////////////////////////////////////////////////////////////////// */ 172 | 173 | #endif /* SYNSOFT_UNIXEM_INCL_H_DIRENT */ 174 | 175 | /* ////////////////////////////////////////////////////////////////////////// */ 176 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Zipper CMake Configuration 3 | # Main configuration file for building the Zipper library with CMake 4 | ############################################################################## 5 | 6 | cmake_minimum_required(VERSION 3.12) 7 | project(zipper VERSION 3.0.2) 8 | 9 | ############################################################################## 10 | # Global compiler settings 11 | ############################################################################## 12 | set(CMAKE_CXX_STANDARD 14) 13 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 14 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 15 | 16 | ############################################################################## 17 | # Build options for enabling/disabling demos and tests 18 | ############################################################################## 19 | option(ZIPPER_BUILD_DEMOS "Build the demo applications" OFF) 20 | option(ZIPPER_BUILD_TESTS "Build the test applications" OFF) 21 | option(ZIPPER_SHARED_LIB "Build zipper as a shared library" OFF) 22 | 23 | ############################################################################## 24 | # External dependencies 25 | ############################################################################## 26 | # Force static libraries for dependencies 27 | set(BUILD_SHARED_LIBS OFF) 28 | 29 | # Configure zlib-ng 30 | set(ZLIB_COMPAT ON CACHE BOOL "Enable zlib-ng compatibility") 31 | set(ZLIB_ENABLE_TESTS OFF CACHE BOOL "Disable zlib-ng tests") 32 | set(SKIP_INSTALL_ALL ON CACHE BOOL "Skip installation of zlib-ng") 33 | add_subdirectory(external/zlib-ng) 34 | 35 | # Configure minizip with AES encryption support 36 | # Define the CMP0077 policy to avoid warning with option() in external/minizip 37 | if(POLICY CMP0077) 38 | cmake_policy(SET CMP0077 NEW) 39 | endif() 40 | option(USE_AES "Enable AES encryption" ON) 41 | add_subdirectory(external/minizip) 42 | target_include_directories(minizip PUBLIC 43 | $ 44 | ) 45 | 46 | ############################################################################## 47 | # Main library targets (static and shared) 48 | ############################################################################## 49 | # Common source files definition 50 | set(ZIPPER_SOURCES 51 | src/utils/Path.cpp 52 | src/utils/Timestamp.cpp 53 | src/utils/glob.cpp 54 | src/Zipper.cpp 55 | src/Unzipper.cpp 56 | ) 57 | 58 | # Compile as static or as shared library 59 | if(ZIPPER_SHARED_LIB) 60 | add_library(zipper SHARED ${ZIPPER_SOURCES}) 61 | else() 62 | add_library(zipper STATIC ${ZIPPER_SOURCES}) 63 | endif() 64 | 65 | # Add platform-specific dependencies 66 | if(WIN32) 67 | target_sources(zipper PRIVATE 68 | src/utils/dirent.c 69 | ) 70 | 71 | target_compile_definitions(zipper PRIVATE 72 | # Prevent Windows' min/max macros from conflicting with std::min/max 73 | NOMINMAX=ON 74 | ) 75 | endif() 76 | 77 | # Export symbols for Windows and DLLs. 78 | if(WIN32 AND ZIPPER_SHARED_LIB) 79 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 80 | set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) 81 | 82 | # Include and setup for exporting symbols 83 | include(GenerateExportHeader) 84 | 85 | # Generate export header for shared library symbols 86 | generate_export_header(zipper 87 | BASE_NAME zipper 88 | EXPORT_MACRO_NAME ZIPPER_EXPORT 89 | EXPORT_FILE_NAME zipper_export.h 90 | ) 91 | target_compile_definitions(zipper PRIVATE zipper_EXPORTS) 92 | target_compile_definitions(zipper PUBLIC ZIPPER_EXPORT_DEFINED) 93 | else() 94 | # For all other cases, undefine the macro 95 | target_compile_options(zipper PRIVATE -UZIPPER_EXPORT_DEFINED) 96 | endif() 97 | 98 | # Include directories configuration 99 | target_include_directories(zipper PRIVATE 100 | ${CMAKE_CURRENT_SOURCE_DIR} 101 | src 102 | external/minizip 103 | ) 104 | 105 | # Public include directories 106 | target_include_directories(zipper PUBLIC 107 | $ 108 | $ 109 | $ 110 | ) 111 | 112 | # Library dependencies 113 | target_link_libraries(zipper PRIVATE 114 | minizip 115 | zlibstatic 116 | ) 117 | 118 | ############################################################################## 119 | # Build demos if CMake option is enabled 120 | ############################################################################## 121 | if(ZIPPER_BUILD_DEMOS) 122 | message(STATUS "Building demo applications") 123 | 124 | # Configure the output directory for demos 125 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) 126 | 127 | # Add demos from their source location 128 | add_subdirectory(doc/demos/Zipper ${CMAKE_BINARY_DIR}/demos/Zipper) 129 | add_subdirectory(doc/demos/Unzipper ${CMAKE_BINARY_DIR}/demos/Unzipper) 130 | endif() 131 | 132 | ############################################################################## 133 | # Build tests if CMake option is enabled (use Google Test) 134 | ############################################################################## 135 | if(ZIPPER_BUILD_TESTS) 136 | message(STATUS "Building test applications") 137 | add_subdirectory(tests) 138 | enable_testing() 139 | endif() 140 | 141 | ############################################################################## 142 | # Configure and install pkg-config file 143 | ############################################################################## 144 | configure_file( 145 | ${CMAKE_CURRENT_SOURCE_DIR}/zipper.pc.in 146 | ${CMAKE_CURRENT_BINARY_DIR}/zipper.pc 147 | @ONLY 148 | ) 149 | 150 | ############################################################################## 151 | # Installation rules 152 | ############################################################################## 153 | include(GNUInstallDirs) 154 | include(CMakePackageConfigHelpers) 155 | 156 | # Configure version information for shared libraries 157 | if(ZIPPER_SHARED_LIB) 158 | set_target_properties(zipper PROPERTIES 159 | VERSION ${PROJECT_VERSION} 160 | SOVERSION ${PROJECT_VERSION_MAJOR} 161 | ) 162 | endif() 163 | 164 | install(TARGETS zipper 165 | EXPORT zipperTargets 166 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 167 | NAMELINK_SKIP 168 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 169 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 170 | ) 171 | 172 | # For shared libraries, add NAMELINK installation 173 | if(ZIPPER_SHARED_LIB) 174 | install(TARGETS zipper 175 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 176 | NAMELINK_ONLY 177 | ) 178 | endif() 179 | 180 | install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 181 | if(WIN32 AND ZIPPER_SHARED_LIB) 182 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/zipper_export.h 183 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 184 | ) 185 | endif() 186 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/zipper.pc 187 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig 188 | ) 189 | 190 | ############################################################################## 191 | # CMake Package Configuration (for find_package support) 192 | ############################################################################## 193 | 194 | # Install the export targets 195 | install(EXPORT zipperTargets 196 | FILE zipperTargets.cmake 197 | NAMESPACE zipper:: 198 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/zipper 199 | ) 200 | 201 | # Generate the config file 202 | set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR}) 203 | configure_package_config_file( 204 | ${CMAKE_CURRENT_SOURCE_DIR}/zipperConfig.cmake.in 205 | ${CMAKE_CURRENT_BINARY_DIR}/zipperConfig.cmake 206 | INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/zipper 207 | PATH_VARS INCLUDE_INSTALL_DIR 208 | ) 209 | 210 | # Generate the version file 211 | write_basic_package_version_file( 212 | ${CMAKE_CURRENT_BINARY_DIR}/zipperConfigVersion.cmake 213 | VERSION ${PROJECT_VERSION} 214 | COMPATIBILITY AnyNewerVersion 215 | ) 216 | 217 | # Install the configuration files 218 | install(FILES 219 | ${CMAKE_CURRENT_BINARY_DIR}/zipperConfig.cmake 220 | ${CMAKE_CURRENT_BINARY_DIR}/zipperConfigVersion.cmake 221 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/zipper 222 | ) 223 | -------------------------------------------------------------------------------- /src/utils/dirent.c: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | //----------------------------------------------------------------------------- 7 | /* ///////////////////////////////////////////////////////////////////////////// 8 | * File: dirent.c 9 | * 10 | * Purpose: Definition of the opendir() API functions for the Win32 platform. 11 | * 12 | * Created 19th October 2002 13 | * Updated: 17th February 2005 14 | * 15 | * Home: http://synesis.com.au/software/ 16 | * 17 | * Copyright 2002-2005, Matthew Wilson and Synesis Software 18 | * All rights reserved. 19 | * 20 | * Redistribution and use in source and binary forms, with or without 21 | * modification, are permitted provided that the following conditions are met: 22 | * 23 | * - Redistributions of source code must retain the above copyright notice, this 24 | * list of conditions and the following disclaimer. 25 | * - Redistributions in binary form must reproduce the above copyright notice, 26 | * this list of conditions and the following disclaimer in the documentation 27 | * and/or other materials provided with the distribution. 28 | * - Neither the names of Matthew Wilson and Synesis Software nor the names of 29 | * any contributors may be used to endorse or promote products derived from 30 | * this software without specific prior written permission. 31 | * 32 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 33 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 34 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 35 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 36 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 37 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 38 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 39 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 40 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 41 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 42 | * POSSIBILITY OF SUCH DAMAGE. 43 | * 44 | * ////////////////////////////////////////////////////////////////////////// */ 45 | 46 | 47 | #ifndef _SYNSOFT_DOCUMENTATION_SKIP_SECTION 48 | # define _SYNSOFT_VER_C_DIRENT_MAJOR 2 49 | # define _SYNSOFT_VER_C_DIRENT_MINOR 0 50 | # define _SYNSOFT_VER_C_DIRENT_REVISION 2 51 | # define _SYNSOFT_VER_C_DIRENT_EDIT 23 52 | #endif /* !_SYNSOFT_DOCUMENTATION_SKIP_SECTION */ 53 | 54 | /* ///////////////////////////////////////////////////////////////////////////// 55 | * Includes 56 | */ 57 | 58 | #include 59 | #include 60 | #include 61 | 62 | #include "dirent.h" 63 | 64 | /* ///////////////////////////////////////////////////////////////////////////// 65 | * Compiler differences 66 | */ 67 | 68 | #if defined(__BORLANDC__) 69 | # define UNIXEM_opendir_PROVIDED_BY_COMPILER 70 | #elif defined(__DMC__) 71 | # define UNIXEM_opendir_PROVIDED_BY_COMPILER 72 | #elif defined(__GNUC__) 73 | # define UNIXEM_opendir_PROVIDED_BY_COMPILER 74 | #elif defined(__INTEL_COMPILER) 75 | #elif defined(_MSC_VER) 76 | #elif defined(__MWERKS__) 77 | #elif defined(__WATCOMC__) 78 | #else 79 | # error Compiler not discriminated 80 | #endif /* compiler */ 81 | 82 | 83 | #if defined(UNIXEM_opendir_PROVIDED_BY_COMPILER) && \ 84 | !defined(UNIXEM_FORCE_ANY_COMPILER) 85 | # error The opendir() API is provided by this compiler, so should not be built here 86 | #endif /* !UNIXEM_opendir_PROVIDED_BY_COMPILER */ 87 | 88 | /* ///////////////////////////////////////////////////////////////////////////// 89 | * Typedefs 90 | */ 91 | 92 | struct dirent_dir 93 | { 94 | char directory[_MAX_DIR + 1]; /* . */ 95 | WIN32_FIND_DATA find_data; /* The Win32 FindFile data. */ 96 | HANDLE hFind; /* The Win32 FindFile handle. */ 97 | struct dirent dirent; /* The handle's entry. */ 98 | }; 99 | 100 | /* ///////////////////////////////////////////////////////////////////////////// 101 | * Helper functions 102 | */ 103 | 104 | static HANDLE findfile_directory(char const *name, LPWIN32_FIND_DATA data) 105 | { 106 | char search_spec[_MAX_PATH +1]; 107 | 108 | /* Simply add the *.*, ensuring the path separator is 109 | * included. 110 | */ 111 | lstrcpyA(search_spec, name); 112 | if(search_spec[lstrlenA(search_spec) - 1] != '\\') 113 | { 114 | lstrcatA(search_spec, "\\*.*"); 115 | } 116 | else 117 | { 118 | lstrcatA(search_spec, "*.*"); 119 | } 120 | 121 | return FindFirstFileA(search_spec, data); 122 | } 123 | 124 | /* ///////////////////////////////////////////////////////////////////////////// 125 | * API functions 126 | */ 127 | 128 | DIR *opendir(char const *name) 129 | { 130 | DIR *result = NULL; 131 | DWORD dwAttr; 132 | 133 | /* Must be a valid name */ 134 | if( !name || 135 | !*name || 136 | (dwAttr = GetFileAttributesA(name)) == 0xFFFFFFFF) 137 | { 138 | errno = ENOENT; 139 | } 140 | /* Must be a directory */ 141 | else if(!(dwAttr & FILE_ATTRIBUTE_DIRECTORY)) 142 | { 143 | errno = ENOTDIR; 144 | } 145 | else 146 | { 147 | result = (DIR*)malloc(sizeof(DIR)); 148 | 149 | if(result == NULL) 150 | { 151 | errno = ENOMEM; 152 | } 153 | else 154 | { 155 | result->hFind = findfile_directory(name, &result->find_data); 156 | 157 | if(result->hFind == INVALID_HANDLE_VALUE) 158 | { 159 | free(result); 160 | 161 | result = NULL; 162 | } 163 | else 164 | { 165 | /* Save the directory, in case of rewind. */ 166 | lstrcpyA(result->directory, name); 167 | lstrcpyA(result->dirent.d_name, result->find_data.cFileName); 168 | } 169 | } 170 | } 171 | 172 | return result; 173 | } 174 | 175 | int closedir(DIR *dir) 176 | { 177 | int ret; 178 | 179 | if(dir == NULL) 180 | { 181 | errno = EBADF; 182 | 183 | ret = -1; 184 | } 185 | else 186 | { 187 | /* Close the search handle, if not already done. */ 188 | if(dir->hFind != INVALID_HANDLE_VALUE) 189 | { 190 | FindClose(dir->hFind); 191 | } 192 | 193 | free(dir); 194 | 195 | ret = 0; 196 | } 197 | 198 | return ret; 199 | } 200 | 201 | void rewinddir(DIR *dir) 202 | { 203 | /* Close the search handle, if not already done. */ 204 | if(dir->hFind != INVALID_HANDLE_VALUE) 205 | { 206 | FindClose(dir->hFind); 207 | } 208 | 209 | dir->hFind = findfile_directory(dir->directory, &dir->find_data); 210 | 211 | if(dir->hFind != INVALID_HANDLE_VALUE) 212 | { 213 | lstrcpyA(dir->dirent.d_name, dir->find_data.cFileName); 214 | } 215 | } 216 | 217 | struct dirent *readdir(DIR *dir) 218 | { 219 | /* The last find exhausted the matches, so return NULL. */ 220 | if(dir->hFind == INVALID_HANDLE_VALUE) 221 | { 222 | errno = EBADF; 223 | 224 | return NULL; 225 | } 226 | else 227 | { 228 | /* Copy the result of the last successful match to 229 | * dirent. 230 | */ 231 | lstrcpyA(dir->dirent.d_name, dir->find_data.cFileName); 232 | 233 | /* Attempt the next match. */ 234 | if(!FindNextFileA(dir->hFind, &dir->find_data)) 235 | { 236 | /* Exhausted all matches, so close and null the 237 | * handle. 238 | */ 239 | FindClose(dir->hFind); 240 | dir->hFind = INVALID_HANDLE_VALUE; 241 | } 242 | 243 | return &dir->dirent; 244 | } 245 | } 246 | 247 | const char* win_strerror(int errnum) 248 | { 249 | static char buffer[256]; 250 | strerror_s(buffer, sizeof(buffer), errnum); 251 | return buffer; 252 | } 253 | 254 | /* ////////////////////////////////////////////////////////////////////////// */ 255 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Non regression tests for Zipper 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | 13 | ############################################################################# 14 | ### Linux Makefile 15 | ############################################################################# 16 | non_regression_linux_makefile: 17 | name: Non regression on Linux Makefile 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout Zipper 21 | uses: actions/checkout@v3 22 | with: 23 | submodules: true 24 | - name: Install system packages 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install pkg-config lcov libdw-dev ninja-build 28 | - name: Download, configure and install Google test 29 | run: | 30 | wget https://github.com/google/googletest/archive/release-1.11.0.tar.gz 31 | tar xf release-1.11.0.tar.gz 32 | cd googletest-release-1.11.0 33 | cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON . 34 | sudo make install 35 | - name: Build Zipper 36 | run: | 37 | V=1 make download-external-libs 38 | V=1 make compile-external-libs 39 | V=1 make -j`nproc --all` 40 | V=1 make demos -j`nproc --all` 41 | - name: Verify installed files 42 | run: | 43 | sudo make install 44 | # Read version from VERSION.txt 45 | VERSION=$(make project-version) 46 | MAJOR=$(echo $VERSION | cut -d. -f1) 47 | MINOR=$(echo $VERSION | cut -d. -f2) 48 | PATCH=$(echo $VERSION | cut -d. -f3) 49 | # Check static and shared libraries 50 | test -f /usr/local/lib/libzipper.a 51 | test -f /usr/local/lib/libzipper.so 52 | test -f /usr/local/lib/libzipper.so.$MAJOR 53 | test -f /usr/local/lib/libzipper.so.$MAJOR.$MINOR.$PATCH 54 | # Check pkg-config file 55 | test -f /usr/local/lib/pkgconfig/zipper.pc 56 | # Check headers 57 | test -f /usr/local/include/zipper/$VERSION/Zipper/Zipper.hpp 58 | test -f /usr/local/include/zipper/$VERSION/Zipper/Unzipper.hpp 59 | test ! -f /usr/local/include/zipper/$VERSION/zipper_export.h 60 | - name: Non regression 61 | run: | 62 | cd tests 63 | V=1 make -j`nproc --all` 64 | ../build/zipper-tests 65 | - name: Check the unzipper demo 66 | run: | 67 | echo "1234" | ./build/unzipper-demo -p -f -o /tmp ./tests/issues/issue_05_password.zip 68 | ls /tmp/issue_05/foo/bar /tmp/issue_05/Nouveau\ dossier/ /tmp/issue_05/Nouveau\ fichier\ vide 69 | - name: Check if the library can be linked against a project 70 | run: | 71 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/ 72 | git clone https://github.com/Lecrapouille/LinkAgainstMyLibs.git --recurse-submodules --depth=1 73 | cd LinkAgainstMyLibs/Zipper 74 | V=1 make -j`nproc --all` 75 | ./build/Zipper 76 | ls ziptest.zip /tmp/somefile.txt 77 | cat /tmp/somefile.txt 78 | 79 | ############################################################################# 80 | ### Linux CMake 81 | ############################################################################# 82 | non_regression_linux_cmake: 83 | name: Non regression on Linux CMake 84 | runs-on: ubuntu-latest 85 | steps: 86 | - name: Checkout Zipper 87 | uses: actions/checkout@v3 88 | with: 89 | submodules: true 90 | - name: Install system packages 91 | run: | 92 | sudo apt-get update 93 | sudo apt-get install pkg-config lcov libdw-dev ninja-build 94 | - name: Build Zipper 95 | run: | 96 | mkdir build 97 | cd build 98 | cmake .. -DZIPPER_SHARED_LIB=ON -DZIPPER_BUILD_DEMOS=ON -DZIPPER_BUILD_TESTS=ON 99 | V=1 make -j`nproc --all` 100 | test -f libzipper.so 101 | test -f zipper-demo 102 | test -f unzipper-demo 103 | test -f zipper-tests 104 | test -f zipper.pc 105 | - name: Verify installed files 106 | run: | 107 | cd build 108 | sudo V=1 make install 109 | test -f /usr/local/lib/libzipper.so 110 | test -f /usr/local/lib/pkgconfig/zipper.pc 111 | test -f /usr/local/include/Zipper/Zipper.hpp 112 | test -f /usr/local/include/Zipper/Unzipper.hpp 113 | - name: Non regression 114 | run: | 115 | cd build 116 | ./zipper-tests 117 | - name: Check the unzipper demo 118 | run: | 119 | cd build 120 | echo "1234" | ./unzipper-demo -p -f -o /tmp ../tests/issues/issue_05_password.zip 121 | ls /tmp/issue_05/foo/bar /tmp/issue_05/Nouveau\ dossier/ /tmp/issue_05/Nouveau\ fichier\ vide 122 | - name: Check if the library can be linked against a project 123 | run: | 124 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib/ 125 | git clone https://github.com/Lecrapouille/LinkAgainstMyLibs.git --recurse-submodules --depth=1 126 | cd LinkAgainstMyLibs/Zipper 127 | V=1 make -j`nproc --all` 128 | ./build/Zipper 129 | ls ziptest.zip /tmp/somefile.txt 130 | cat /tmp/somefile.txt 131 | - name: Check if the library can be linked against a project using CMake 132 | run: | 133 | cd doc/demos/CMakeHelloWorld 134 | cmake . 135 | cmake --build . 136 | ./test_zipper 137 | 138 | ############################################################################# 139 | ### MacOS X Makefile 140 | ############################################################################# 141 | non_regression_macos_makefile: 142 | name: Non regression on MacOS X Makefile 143 | runs-on: macos-latest 144 | steps: 145 | - name: Checkout Zipper 146 | uses: actions/checkout@v3 147 | with: 148 | submodules: true 149 | - name: Install system packages 150 | run: | 151 | brew install pkg-config ninja 152 | - name: Download, configure and install Google test 153 | run: | 154 | wget https://github.com/google/googletest/archive/release-1.11.0.tar.gz 155 | tar xf release-1.11.0.tar.gz 156 | cd googletest-release-1.11.0 157 | cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON . 158 | sudo make install 159 | - name: Build Zipper 160 | run: | 161 | V=1 make download-external-libs 162 | V=1 make compile-external-libs 163 | V=1 make -j`sysctl -n hw.logicalcpu` 164 | - name: Verify installed files 165 | run: | 166 | sudo make install 167 | # Read version from VERSION.txt 168 | VERSION=$(make project-version) 169 | # Check static and shared libraries 170 | test -f /usr/local/lib/libzipper.a 171 | test -f /usr/local/lib/libzipper.dylib 172 | # Check pkg-config file 173 | test -f /usr/local/lib/pkgconfig/zipper.pc 174 | # Check headers 175 | test -f /usr/local/include/zipper/$VERSION/Zipper/Zipper.hpp 176 | test -f /usr/local/include/zipper/$VERSION/Zipper/Unzipper.hpp 177 | test ! -f /usr/local/include/zipper/$VERSION/zipper_export.h 178 | - name: Non regression 179 | run: | 180 | cd tests 181 | V=1 make -j`nproc --all` 182 | ../build/zipper-tests 183 | - name: Check the unzipper demo 184 | run: | 185 | echo "1234" | ./build/unzipper-demo -p -f -o /tmp ./tests/issues/issue_05_password.zip 186 | ls /tmp/issue_05/foo/bar /tmp/issue_05/Nouveau\ dossier/ /tmp/issue_05/Nouveau\ fichier\ vide 187 | - name: Check if the library can be linked against a project 188 | run: | 189 | git clone https://github.com/Lecrapouille/LinkAgainstMyLibs.git --recurse-submodules --depth=1 190 | cd LinkAgainstMyLibs/Zipper 191 | V=1 make -j`nproc --all` 192 | ./build/Zipper 193 | ls ziptest.zip /tmp/somefile.txt 194 | cat /tmp/somefile.txt 195 | 196 | ############################################################################# 197 | ### Windows CMake 198 | ############################################################################# 199 | non_regression_windows_cmake: 200 | name: Non regression on Windows CMake 201 | runs-on: windows-2022 202 | steps: 203 | - name: Add path for finding cl.exe 204 | uses: ilammy/msvc-dev-cmd@v1.10.0 205 | - name: Checkout Zipper 206 | uses: actions/checkout@v3 207 | with: 208 | submodules: true 209 | - name: Build Zipper shared with CMake 210 | shell: powershell 211 | run: | 212 | mkdir build 213 | cd build 214 | cmake .. -DZIPPER_SHARED_LIB=ON -DZIPPER_BUILD_DEMOS=ON -DZIPPER_BUILD_TESTS=ON 215 | cmake --build . --config Release 216 | Test-Path -Path ".\Release\zipper.dll" 217 | Test-Path -Path ".\Release\zipper.lib" 218 | - name: Install and verify files 219 | shell: powershell 220 | run: | 221 | cmake --install build --prefix "C:\Program Files\zipper_shared" 222 | # Check for shared library files 223 | Test-Path -Path "C:\Program Files\zipper_shared\bin\zipper.dll" 224 | Test-Path -Path "C:\Program Files\zipper_shared\lib\zipper.lib" 225 | # Check pkg-config file 226 | Test-Path -Path "C:\Program Files\zipper_shared\lib\pkgconfig\zipper.pc" 227 | # Check headers 228 | Test-Path -Path "C:\Program Files\zipper_shared\include\Zipper\Zipper.hpp" 229 | Test-Path -Path "C:\Program Files\zipper_shared\include\Zipper\Unzipper.hpp" 230 | Test-Path -Path "C:\Program Files\zipper_shared\include\zipper_export.h" 231 | - name: Non regression 232 | shell: powershell 233 | run: | 234 | cd build\Release 235 | .\zipper-tests.exe 236 | - name: Check if the library can be linked against a project using CMake 237 | shell: powershell 238 | run: | 239 | cd doc/demos/CMakeHelloWorld 240 | cmake . 241 | cmake --build . --config Release 242 | # FIXME 243 | # .\Release\test_zipper.exe -------------------------------------------------------------------------------- /src/utils/Path.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | // 7 | // Copyright (C) 2008 by Pedro Mendes, Virginia Tech Intellectual 8 | // Properties, Inc., EML Research, gGmbH, University of Heidelberg, 9 | // and The University of Manchester. 10 | // All rights reserved. 11 | // 12 | // Copyright (C) 2001 - 2007 by Pedro Mendes, Virginia Tech Intellectual 13 | // Properties, Inc. and EML Research, gGmbH. 14 | // All rights reserved. 15 | //----------------------------------------------------------------------------- 16 | 17 | #ifndef ZIPPER_UTILS_PATH_HPP 18 | #define ZIPPER_UTILS_PATH_HPP 19 | 20 | #include 21 | #include 22 | 23 | // ***************************************************************************** 24 | // Generated by CMake. Needed for exporting symbols for unit tests with Windows 25 | // if compiled as shared library. 26 | // FIXME: Ideally this class should not be exported since it is not part of the 27 | // public API. https://github.com/Lecrapouille/zipper/issues/21 28 | // ***************************************************************************** 29 | #if defined(ZIPPER_EXPORT_DEFINED) 30 | # include "zipper_export.h" 31 | #else 32 | # define ZIPPER_EXPORT 33 | #endif 34 | namespace zipper 35 | { 36 | 37 | // ***************************************************************************** 38 | //! \brief This class provides an OS independent interface to directory entries 39 | //! such as files and directories. 40 | // ***************************************************************************** 41 | class ZIPPER_EXPORT Path 42 | { 43 | public: 44 | 45 | //! \brief Returns the preferred separator for the current OS 46 | //! \param[in] path Path to get the preferred separator from 47 | //! \return char Preferred separator 48 | static char preferredSeparator(const std::string& path); 49 | 50 | //! \brief Convert any path separators to the native format for the current 51 | //! OS \param[in] path Path with potentially mixed separators \return 52 | //! std::string Path with consistent OS-specific separators 53 | static std::string toNativeSeparators(const std::string& path); 54 | 55 | //! \brief Convert all path separators to Unix format (forward slash) 56 | //! \param[in] path Path to convert 57 | //! \return std::string Path with Unix separators 58 | static std::string toUnixSeparators(const std::string& path); 59 | 60 | //! \brief Convert all path separators to Windows format (backslash) 61 | //! \param[in] path Path to convert 62 | //! \return std::string Path with Windows separators 63 | static std::string toWindowsSeparators(const std::string& path); 64 | 65 | //! \brief Standardize path separators for storage in ZIP archives 66 | //! This method converts all separators to the format most commonly used in 67 | //! ZIP archives (forward slash) \param[in] path Path to standardize \return 68 | //! std::string Path with standardized separators for ZIP storage 69 | static std::string toZipArchiveSeparators(const std::string& path); 70 | 71 | //! \brief Detect if a path contains mixed separators 72 | //! \param[in] path Path to check 73 | //! \return bool True if path contains both Windows and Unix separators 74 | static bool hasMixedSeparators(const std::string& path); 75 | 76 | //! \brief Returns the root of the path. 77 | //! \param[in] path Path to get the root of 78 | //! \return std::string Root of the path 79 | static std::string root(const std::string& path); 80 | 81 | //! \brief Check if the path is a root. 82 | //! \param[in] path Path to check 83 | //! \return bool True if path is a root 84 | static bool isRoot(const std::string& path); 85 | 86 | //! \brief Returns the current working directory path. 87 | //! \details This function returns the current working directory path with 88 | //! the following features: 89 | //! - Uses dynamic buffer allocation to handle long paths 90 | //! - Normalizes path separators to the native format 91 | //! - Handles errors gracefully 92 | //! - Supports paths up to 32KB in length 93 | //! \return std::string Current working directory path, or empty string on 94 | //! error 95 | static std::string currentPath(); 96 | 97 | //! \brief Check whether the directory entry specified by 'path' is 98 | //! a file. 99 | //! \param[in] path: file path. 100 | //! \return bool isFile 101 | static bool isFile(const std::string& path); 102 | 103 | //! \brief Check whether the directory entry specified by 'path' is 104 | //! is a directory. 105 | //! \param[in] path: file path. 106 | //! \return bool isDir 107 | static bool isDir(const std::string& path); 108 | 109 | //! \brief Returns the directory name with the preferred separator 110 | //! \param[in] folder_path: folder path 111 | //! \return std::string directory name with preferred separator 112 | static std::string folderNameWithSeparator(const std::string& folder_path); 113 | 114 | //! \brief Check whether the directory entry specified by 'path' exists. 115 | //! \param[in] path: file path. 116 | //! \return bool exist 117 | static bool exist(const std::string& path); 118 | 119 | //! \brief Check whether the directory entry specified by 'path' is 120 | //! is readable. 121 | //! \param[in] path: file path. 122 | //! \return bool isReadable 123 | static bool isReadable(const std::string& path); 124 | 125 | //! \brief Check whether the directory entry specified by 'path' is 126 | //! writable. 127 | //! \param[in] path: file path. 128 | //! \return bool isWritable 129 | static bool isWritable(const std::string& path); 130 | 131 | //! \brief Returns the base name, i.e., the directory path and the 132 | //! the suffix are removed from 'path'. 133 | //! \param[in] path: file path. 134 | //! \return std::string baseName 135 | // static std::string baseName(const std::string& path); 136 | 137 | //! \brief Returns the file name, i.e., the directory path is removed from 138 | //! 'path'. \param[in] path: file path. \return std::string fileName 139 | static std::string fileName(const std::string& path); 140 | 141 | //! \brief Returns the directory path to the parent directoryu, i.e., 142 | //! the file name or directory name are removed from 'path'. 143 | //! \param[in] path: file path. 144 | //! \return std::string dirName 145 | static std::string dirName(const std::string& path); 146 | 147 | //! \brief Returns the extension, i.e., the directory path and the 148 | //! the base name are removed from 'path'. 149 | //! \param[in] path: file path. 150 | //! \return std::string extension 151 | static std::string extension(const std::string& path); 152 | 153 | //! \brief Create the directory 'dir' in the parent directory 'parent'. 154 | //! \param[in] dir: folder path. 155 | //! \param[in] parent (Default: current working directory) 156 | //! \return bool success 157 | static bool createDir(const std::string& dir, 158 | const std::string& parent = ""); 159 | 160 | static void removeDir(const std::string& foldername); 161 | 162 | //! \brief Return the list of the folder. If recurse == false then stay 163 | //! inside the first depth. 164 | static std::vector filesFromDir(const std::string& path, 165 | const bool recurse); 166 | 167 | //! \brief Return the temporary directory for the current OS 168 | //! \return std::string Temporary directory 169 | static std::string getTempDirectory(); 170 | 171 | //! \brief Create a name for a temporary directory entry. The directory 172 | //! entry will be located in the directory given \param[in] dir: folder 173 | //! path. \param[in] suffix: file extension. \return std::string tmpName 174 | static std::string createTempName(const std::string& dir, 175 | const std::string& suffix); 176 | 177 | //! \brief Removes a file or directory specified by path. 178 | //! \param[in] path: file path. 179 | //! \return bool success 180 | 181 | static bool remove(const std::string& path); 182 | 183 | //! \brief Checks whether the given path is relative 184 | //! \param[in] path: file path. 185 | //! \return bool isRelative 186 | static bool isRelativePath(const std::string& path); 187 | 188 | //! \brief This substitute ../ ie foo/../bar will return bar 189 | static std::string normalize(const std::string& path); 190 | 191 | //! \brief Returns the canonical path of the destination directory. 192 | //! \param[in] p_destination_dir The destination directory path 193 | //! \return std::string Canonical path of the destination directory 194 | static std::string canonicalPath(const std::string& p_destination_dir); 195 | 196 | static bool isLargeFile(std::istream& input_stream); 197 | 198 | //! \biref Check if the file name ends with a backslash or slash char 199 | static bool hasTrailingSlash(const std::string& path); 200 | 201 | //! \brief Get the size of a file in bytes. 202 | //! \param[in] path Path to the file. 203 | //! \return size_t File size in bytes, or 0 if the file does not exist or is 204 | //! not a file. 205 | static size_t getFileSize(const std::string& path); 206 | 207 | //! \brief Checks if a ZIP entry could be used for a zip slip attack 208 | //! \param[in] p_entry_path The path inside the ZIP file 209 | //! \param[in] p_target_dir The destination directory path 210 | //! \return true if the entry could escape the destination directory 211 | static bool isZipSlip(const std::string& p_entry_path, 212 | const std::string& p_target_dir); 213 | 214 | enum class InvalidEntryReason 215 | { 216 | VALID_ENTRY = 0, 217 | EMPTY_ENTRY = 1, 218 | FORBIDDEN_CHARACTERS = 2, 219 | CONTROL_CHARACTERS = 3, 220 | ABSOLUTE_PATH = 4, 221 | ZIP_SLIP = 5 222 | }; 223 | 224 | //! \brief Checks if a ZIP entry name has control characters 225 | //! \param[in] p_entry_name The entry name to check 226 | //! \return true if the entry name has control characters 227 | static InvalidEntryReason 228 | checkControlCharacters(const std::string& p_entry_name); 229 | 230 | //! \brief Checks if a ZIP entry name is valid 231 | //! \param[in] p_entry_name The entry name to check 232 | //! \return true if the entry name is valid 233 | static InvalidEntryReason isValidEntry(const std::string& p_entry_name); 234 | 235 | //! \brief Returns a string describing the reason why a ZIP entry name is 236 | //! invalid 237 | //! \param[in] p_reason The reason why the entry name is invalid 238 | //! \return std::string Description of the reason 239 | static std::string getInvalidEntryReason(InvalidEntryReason p_reason); 240 | }; 241 | 242 | } // namespace zipper 243 | 244 | #endif // ZIPPER_UTILS_PATH_HPP -------------------------------------------------------------------------------- /tests/TestSecurity.cpp: -------------------------------------------------------------------------------- 1 | #include "gmock/gmock.h" 2 | #include "gtest/gtest.h" 3 | 4 | #define protected public 5 | #define private public 6 | #include "Zipper/Unzipper.hpp" 7 | #include "Zipper/Zipper.hpp" 8 | #undef protected 9 | #undef private 10 | 11 | #include "TestHelper.hpp" 12 | 13 | using namespace zipper; 14 | 15 | #ifndef PWD 16 | # error "PWD shall be defined" 17 | #endif 18 | 19 | //============================================================================= 20 | // Tests for zip-slip vulnerability 21 | // From https://github.com/snyk/zip-slip-vulnerability/tree/master 22 | //============================================================================= 23 | TEST(ZipSlipTests, ZipSlip) 24 | { 25 | #if defined(_WIN32) 26 | std::string zip_path(PWD "/issues/zip-slip-win.zip"); 27 | #else 28 | std::string zip_path(PWD "/issues/zip-slip-linux.zip"); 29 | #endif 30 | std::string temp_dir = "zip-slip-test/"; 31 | 32 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 33 | 34 | Unzipper unzipper(zip_path); 35 | ASSERT_FALSE(unzipper.extractAll(temp_dir)); 36 | ASSERT_THAT(unzipper.error().message(), 37 | testing::HasSubstr("Security error")); 38 | unzipper.close(); 39 | 40 | // Check that the good file is present. 41 | // Check that the evil file is not present. 42 | ASSERT_TRUE(helper::checkFileExists("zip-slip-test/good.txt", 43 | "this is a good one\n")); 44 | ASSERT_TRUE(helper::checkFileDoesNotExist("zip-slip-test/evil.txt")); 45 | 46 | // Clean up 47 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 48 | } 49 | 50 | //============================================================================= 51 | // https://github.com/Lecrapouille/zipper/issues/7 52 | // https://unforgettable.dk/ 53 | //============================================================================= 54 | TEST(ZipSlipTests, Forty42) 55 | { 56 | std::string zip_path(PWD "/issues/42.zip"); 57 | std::string temp_dir = "42/"; 58 | std::string password = "42"; 59 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 60 | 61 | Unzipper unzipper(zip_path, password); 62 | auto entries = unzipper.entries(); 63 | ASSERT_EQ(entries.size(), 16); 64 | ASSERT_EQ(entries[0].name, "lib 0.zip"); 65 | ASSERT_EQ(entries[1].name, "lib 1.zip"); 66 | ASSERT_EQ(entries[2].name, "lib 2.zip"); 67 | ASSERT_EQ(entries[3].name, "lib 3.zip"); 68 | ASSERT_EQ(entries[4].name, "lib 4.zip"); 69 | ASSERT_EQ(entries[5].name, "lib 5.zip"); 70 | ASSERT_EQ(entries[6].name, "lib 6.zip"); 71 | ASSERT_EQ(entries[7].name, "lib 7.zip"); 72 | ASSERT_EQ(entries[8].name, "lib 8.zip"); 73 | ASSERT_EQ(entries[9].name, "lib 9.zip"); 74 | ASSERT_EQ(entries[10].name, "lib a.zip"); 75 | ASSERT_EQ(entries[11].name, "lib b.zip"); 76 | ASSERT_EQ(entries[12].name, "lib c.zip"); 77 | ASSERT_EQ(entries[13].name, "lib d.zip"); 78 | ASSERT_EQ(entries[14].name, "lib e.zip"); 79 | ASSERT_EQ(entries[15].name, "lib f.zip"); 80 | ASSERT_TRUE(unzipper.extractAll(temp_dir)); 81 | unzipper.close(); 82 | 83 | ASSERT_TRUE(helper::checkFileExists("42/lib 0.zip")); 84 | ASSERT_TRUE(helper::checkFileExists("42/lib 1.zip")); 85 | ASSERT_TRUE(helper::checkFileExists("42/lib 2.zip")); 86 | ASSERT_TRUE(helper::checkFileExists("42/lib 3.zip")); 87 | ASSERT_TRUE(helper::checkFileExists("42/lib 4.zip")); 88 | ASSERT_TRUE(helper::checkFileExists("42/lib 5.zip")); 89 | ASSERT_TRUE(helper::checkFileExists("42/lib 6.zip")); 90 | ASSERT_TRUE(helper::checkFileExists("42/lib 7.zip")); 91 | ASSERT_TRUE(helper::checkFileExists("42/lib 8.zip")); 92 | ASSERT_TRUE(helper::checkFileExists("42/lib 9.zip")); 93 | ASSERT_TRUE(helper::checkFileExists("42/lib a.zip")); 94 | ASSERT_TRUE(helper::checkFileExists("42/lib b.zip")); 95 | ASSERT_TRUE(helper::checkFileExists("42/lib c.zip")); 96 | ASSERT_TRUE(helper::checkFileExists("42/lib d.zip")); 97 | ASSERT_TRUE(helper::checkFileExists("42/lib e.zip")); 98 | ASSERT_TRUE(helper::checkFileExists("42/lib f.zip")); 99 | 100 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 101 | } 102 | 103 | //============================================================================= 104 | // https://github.com/Lecrapouille/zipper/issues/7 105 | // https://www.bamsoftware.com/hacks/zipbomb/ 106 | //============================================================================= 107 | TEST(ZipSlipTests, ZipBomb) 108 | { 109 | std::string zip_path(PWD "/issues/zbsm.zip"); 110 | std::string temp_dir = "zip-bomb/"; 111 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 112 | 113 | Unzipper unzipper(zip_path); 114 | auto entries = unzipper.entries(); 115 | ASSERT_EQ(entries.size(), 250); 116 | for (size_t i = 0; i < entries.size(); ++i) 117 | { 118 | ASSERT_EQ(entries[i].name, helper::intToBase36(i)); 119 | } 120 | ASSERT_EQ(unzipper.sizeOnDisk(), 5461307620); // 5.46 GB 121 | unzipper.close(); 122 | 123 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 124 | } 125 | 126 | #if 0 127 | //============================================================================= 128 | // This is not a test but just an helper to create a nasty zip file. You should 129 | // deactivate the security before. And 130 | //============================================================================= 131 | TEST(ZipSlipTests, CreateNastyZipFile) 132 | { 133 | std::vector forbidden_entries = { 134 | "corr<.txt", "corr>.txt", "corr:.txt", "corr\".txt", 135 | "corr|.txt", "corr*.txt", "corr?.txt", 136 | }; 137 | 138 | std::string zip_path(PWD "/issues/nasty.zip"); 139 | Zipper zipper(zip_path, Zipper::OpenFlags::Overwrite); 140 | 141 | // Forbidden characters 142 | for (const auto& forbidden_entry : forbidden_entries) 143 | { 144 | ASSERT_TRUE(helper::zipAddFile( 145 | zipper, "corrupted.txt", "corrupted", forbidden_entry.c_str())); 146 | } 147 | // Control characters 148 | ASSERT_TRUE(helper::zipAddFile( 149 | zipper, "corrupted.txt", "corrupted", "\x00corrupted.txt")); 150 | 151 | // Absolute path: allowed: the leading slash is removed 152 | ASSERT_TRUE(helper::zipAddFile( 153 | zipper, "corrupted.txt", "corrupted", "/foo/bar/corrupted1.txt")); 154 | 155 | // Zip slip: allowed: the leading slash is removed 156 | ASSERT_TRUE(helper::zipAddFile( 157 | zipper, "corrupted.txt", "corrupted", "/../corrupted2.txt")); 158 | 159 | // Zip slip: not allowed: the leading slash is not removed 160 | ASSERT_TRUE(helper::zipAddFile( 161 | zipper, "corrupted.txt", "corrupted", "../corrupted3.txt")); 162 | 163 | zipper.close(); 164 | 165 | // Check contents 166 | size_t i = 0; 167 | Unzipper unzipper(zip_path); 168 | ASSERT_EQ(unzipper.entries().size(), forbidden_entries.size() + 4u); 169 | for (i = 0; i < forbidden_entries.size(); ++i) 170 | { 171 | ASSERT_EQ(unzipper.entries()[i].name, forbidden_entries[i]); 172 | } 173 | ASSERT_STREQ(unzipper.entries()[i].name.c_str(), "\x00corrupted.txt"); 174 | ASSERT_STREQ(unzipper.entries()[i + 1].name.c_str(), 175 | "/foo/bar/corrupted1.txt"); 176 | ASSERT_STREQ(unzipper.entries()[i + 2].name.c_str(), "/../corrupted2.txt"); 177 | ASSERT_STREQ(unzipper.entries()[i + 3].name.c_str(), "../corrupted3.txt"); 178 | unzipper.close(); 179 | } 180 | #endif 181 | 182 | //============================================================================= 183 | // Try add corrupted file to zip 184 | //============================================================================= 185 | TEST(ZipSlipTests, CorruptedFile) 186 | { 187 | std::vector forbidden_entries = { 188 | "corr<.txt", "corr>.txt", "corr:.txt", "corr\".txt", 189 | "corr|.txt", "corr*.txt", "corr?.txt", 190 | }; 191 | 192 | Zipper zipper("corrupted.zip", Zipper::OpenFlags::Overwrite); 193 | 194 | // Forbidden characters 195 | for (const auto& forbidden_entry : forbidden_entries) 196 | { 197 | ASSERT_TRUE(helper::zipAddFile( 198 | zipper, "corrupted.txt", "corrupted", forbidden_entry.c_str())); 199 | } 200 | 201 | // Control characters 202 | ASSERT_FALSE(helper::zipAddFile( 203 | zipper, "corrupted.txt", "corrupted", "\x00corrupted.txt")); 204 | ASSERT_THAT(zipper.error().message(), 205 | testing::HasSubstr("contains control characters")); 206 | 207 | // Absolute path: allowed: the leading slash is removed 208 | ASSERT_TRUE(helper::zipAddFile( 209 | zipper, "corrupted.txt", "corrupted", "/foo/bar/corrupted1.txt")); 210 | 211 | // Zip slip: allowed: the leading slash is removed 212 | ASSERT_TRUE(helper::zipAddFile( 213 | zipper, "corrupted.txt", "corrupted", "/../corrupted2.txt")); 214 | 215 | // Zip slip: not allowed: the leading slash is not removed 216 | ASSERT_FALSE(helper::zipAddFile( 217 | zipper, "corrupted.txt", "corrupted", "../corrupted3.txt")); 218 | ASSERT_THAT(zipper.error().message(), 219 | testing::HasSubstr( 220 | "could be used to escape the destination directory")); 221 | zipper.close(); 222 | 223 | // Check contents 224 | Unzipper unzipper("corrupted.zip"); 225 | ASSERT_EQ(unzipper.entries().size(), 9u); 226 | ASSERT_EQ(unzipper.entries()[0].name, "corr<.txt"); 227 | ASSERT_EQ(unzipper.entries()[1].name, "corr>.txt"); 228 | ASSERT_EQ(unzipper.entries()[2].name, "corr:.txt"); 229 | ASSERT_EQ(unzipper.entries()[3].name, "corr\".txt"); 230 | ASSERT_EQ(unzipper.entries()[4].name, "corr|.txt"); 231 | ASSERT_EQ(unzipper.entries()[5].name, "corr*.txt"); 232 | ASSERT_EQ(unzipper.entries()[6].name, "corr?.txt"); 233 | ASSERT_EQ(unzipper.entries()[7].name, "foo/bar/corrupted1.txt"); 234 | ASSERT_EQ(unzipper.entries()[8].name, "corrupted2.txt"); 235 | unzipper.close(); 236 | 237 | ASSERT_TRUE(helper::removeFileOrDir("corrupted.zip")); 238 | } 239 | 240 | //============================================================================= 241 | // Try extract corrupted file from zip 242 | //============================================================================= 243 | TEST(ZipSlipTests, CorruptedFileExtraction) 244 | { 245 | std::string zip_path(PWD "/issues/nasty.zip"); 246 | std::string temp_dir = "corrupted/"; 247 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 248 | 249 | Unzipper unzipper(zip_path); 250 | 251 | auto entries = unzipper.entries(); 252 | ASSERT_EQ(entries.size(), 5u); 253 | ASSERT_EQ(entries[0].name, "corr*.txt"); 254 | ASSERT_EQ(entries[1].name, "\x00corrupted.txt"); 255 | ASSERT_EQ(entries[2].name, "/foo/bar/corrupted1.txt"); 256 | ASSERT_EQ(entries[3].name, "/../corrupted2.txt"); 257 | ASSERT_EQ(entries[4].name, "../corrupted3.txt"); 258 | 259 | // Check that the extraction fails 260 | ASSERT_FALSE(unzipper.extractAll(temp_dir)); 261 | ASSERT_THAT(unzipper.error().message(), 262 | testing::HasSubstr("Security error")); 263 | std::cout << "error: " << unzipper.error().message() << std::endl; 264 | 265 | // Check that the extraction fails for each entry 266 | for (size_t i = 1u; i < entries.size(); ++i) 267 | { 268 | std::cout << "Extracting: " << entries[i].name << std::endl; 269 | ASSERT_FALSE(unzipper.extract( 270 | entries[i].name, temp_dir, Unzipper::OverwriteMode::Overwrite)); 271 | ASSERT_THAT(unzipper.error().message(), 272 | testing::HasSubstr("Security error")); 273 | 274 | ASSERT_FALSE(unzipper.extract(entries[i].name, 275 | Unzipper::OverwriteMode::Overwrite)); 276 | ASSERT_THAT(unzipper.error().message(), 277 | testing::HasSubstr("Security error")); 278 | } 279 | 280 | // The entry 0 is extracted without error 281 | std::cout << "Extracting: " << entries[0].name << std::endl; 282 | #ifdef _WIN32 283 | // FIXME: https://github.com/Lecrapouille/zipper/issues/30 284 | ASSERT_FALSE(unzipper.extract( 285 | entries[0].name, temp_dir, Unzipper::OverwriteMode::Overwrite)); 286 | std::cout << "error: " << unzipper.error().message() << std::endl; 287 | ASSERT_FALSE( 288 | unzipper.extract(entries[0].name, Unzipper::OverwriteMode::Overwrite)); 289 | #else 290 | ASSERT_TRUE(unzipper.extract( 291 | entries[0].name, temp_dir, Unzipper::OverwriteMode::Overwrite)); 292 | ASSERT_TRUE( 293 | unzipper.extract(entries[0].name, Unzipper::OverwriteMode::Overwrite)); 294 | #endif 295 | 296 | unzipper.close(); 297 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 298 | ASSERT_TRUE(helper::removeFileOrDir(entries[0].name)); 299 | } -------------------------------------------------------------------------------- /tests/TestZipMemory.cpp: -------------------------------------------------------------------------------- 1 | #include "gmock/gmock.h" 2 | #include "gtest/gtest.h" 3 | 4 | #define protected public 5 | #define private public 6 | #include "Zipper/Unzipper.hpp" 7 | #include "Zipper/Zipper.hpp" 8 | #undef protected 9 | #undef private 10 | 11 | #include "TestHelper.hpp" 12 | 13 | using namespace zipper; 14 | 15 | //============================================================================= 16 | // Tests for memory zip operations 17 | //============================================================================= 18 | TEST(ZipperMemoryOps, ZipToVector) 19 | { 20 | std::vector zipData; 21 | const std::string entryName = "vector_entry.txt"; 22 | const std::string content = "vector content"; 23 | 24 | // Constructor for vector 25 | { 26 | ASSERT_TRUE(zipData.empty()); 27 | 28 | Zipper zipper(zipData); 29 | ASSERT_TRUE(zipData.empty()); 30 | 31 | std::stringstream contentStream(content); 32 | ASSERT_TRUE(zipper.add(contentStream, entryName)); 33 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 34 | ASSERT_TRUE(zipData.empty()); 35 | 36 | zipper.close(); 37 | ASSERT_FALSE(zipData.empty()); 38 | } 39 | 40 | // Verify content using Unzipper 41 | { 42 | Unzipper unzipper(zipData); 43 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 44 | auto entries = unzipper.entries(); 45 | ASSERT_EQ(entries.size(), 1); 46 | ASSERT_EQ(entries[0].name, entryName); 47 | 48 | std::vector extractedData; 49 | ASSERT_TRUE(unzipper.extract(entryName, extractedData)); 50 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 51 | std::string extractedString(extractedData.begin(), extractedData.end()); 52 | ASSERT_EQ(extractedString, content); 53 | unzipper.close(); 54 | } 55 | 56 | // Open with the same non empty vector 57 | { 58 | Zipper zipper(zipData); 59 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 60 | zipper.close(); 61 | 62 | Unzipper unzipper(zipData); 63 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 64 | auto entries = unzipper.entries(); 65 | ASSERT_EQ(entries.size(), 1); 66 | ASSERT_EQ(entries[0].name, entryName); 67 | unzipper.close(); 68 | } 69 | } 70 | 71 | //============================================================================= 72 | // Tests for zip multiple entries to vector 73 | //============================================================================= 74 | TEST(ZipperMemoryOps, ZipMultipleToVector) 75 | { 76 | std::vector zipData; 77 | const std::string entryName1 = "multi_vec1.txt"; 78 | const std::string content1 = "multi vec content 1"; 79 | const std::string entryName2 = "folder/multi_vec2.dat"; 80 | const std::string content2 = "multi vec content 2"; 81 | 82 | // Constructor for vector 83 | { 84 | ASSERT_TRUE(zipData.empty()); 85 | 86 | Zipper zipper(zipData); 87 | ASSERT_TRUE(zipData.empty()); 88 | 89 | std::stringstream stream1(content1); 90 | std::stringstream stream2(content2); 91 | ASSERT_TRUE(zipper.add(stream1, entryName1)); 92 | ASSERT_TRUE(zipper.add(stream2, entryName2)); 93 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 94 | ASSERT_TRUE(zipData.empty()); 95 | 96 | zipper.close(); 97 | ASSERT_FALSE(zipData.empty()); 98 | } 99 | 100 | // Verify content using Unzipper 101 | { 102 | Unzipper unzipper(zipData); 103 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 104 | auto entries = unzipper.entries(); 105 | ASSERT_EQ(entries.size(), 2); 106 | 107 | std::vector data1, data2; 108 | ASSERT_TRUE(unzipper.extract(entryName1, data1)); 109 | ASSERT_EQ(std::string(data1.begin(), data1.end()), content1); 110 | ASSERT_TRUE(unzipper.extract(entryName2, data2)); 111 | ASSERT_EQ(std::string(data2.begin(), data2.end()), content2); 112 | unzipper.close(); 113 | } 114 | } 115 | 116 | //============================================================================= 117 | // Tests for zip with password to vector 118 | //============================================================================= 119 | TEST(ZipperMemoryOps, ZipWithPasswordToVector) 120 | { 121 | std::vector zipData; 122 | const std::string entryName = "pwd_vec.txt"; 123 | const std::string content = "pwd vec content"; 124 | const std::string password = "memory_password"; 125 | 126 | // Use password constructor 127 | { 128 | Zipper zipper(zipData, password); 129 | std::stringstream contentStream(content); 130 | ASSERT_TRUE(zipper.add(contentStream, entryName)); 131 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 132 | zipper.close(); 133 | } 134 | ASSERT_FALSE(zipData.empty()); 135 | 136 | // Verify with Unzipper (correct password) 137 | { 138 | Unzipper unzipper(zipData, password); 139 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 140 | std::vector data; 141 | ASSERT_TRUE(unzipper.extract(entryName, data)); 142 | ASSERT_EQ(std::string(data.begin(), data.end()), content); 143 | unzipper.close(); 144 | } 145 | 146 | // Verify with Unzipper (wrong password) 147 | { 148 | Unzipper unzipper(zipData, "wrong_pwd"); 149 | ASSERT_FALSE(unzipper.error()) 150 | << "Constructor should not fail on wrong password."; 151 | std::vector data; 152 | ASSERT_FALSE( 153 | unzipper.extract(entryName, data)); // Extraction should fail 154 | ASSERT_TRUE(unzipper.error()); 155 | ASSERT_TRUE(data.empty()); 156 | unzipper.close(); 157 | } 158 | } 159 | 160 | //============================================================================= 161 | // Tests for zip from external file to vector 162 | //============================================================================= 163 | TEST(ZipperMemoryOps, ZipFromExternalFileToVector) 164 | { 165 | // Test adding a file from disk when zipper target is memory 166 | std::vector zipData; 167 | const std::string tempFileName = "temp_file_for_memzip.txt"; 168 | const std::string content = "disk file to memory zip"; 169 | const std::string entryName = "disk_file_entry.txt"; 170 | 171 | ASSERT_TRUE(helper::createFile(tempFileName, content)); 172 | 173 | { 174 | Zipper zipper(zipData); 175 | std::ifstream ifs(tempFileName, std::ios::binary); 176 | ASSERT_TRUE(ifs.is_open()) 177 | << "Failed to open temporary file: " << tempFileName; 178 | ASSERT_TRUE(zipper.add(ifs, entryName)); 179 | ifs.close(); 180 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 181 | zipper.close(); 182 | } 183 | 184 | ASSERT_TRUE(helper::removeFileOrDir(tempFileName)); 185 | ASSERT_FALSE(zipData.empty()); 186 | 187 | // Verify content using Unzipper 188 | { 189 | Unzipper unzipper(zipData); 190 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 191 | ASSERT_EQ(unzipper.entries().size(), 1); 192 | ASSERT_EQ(unzipper.entries()[0].name, entryName); 193 | 194 | std::vector data; 195 | ASSERT_TRUE(unzipper.extract(entryName, data)); 196 | ASSERT_EQ(std::string(data.begin(), data.end()), content); 197 | unzipper.close(); 198 | } 199 | } 200 | 201 | //============================================================================= 202 | // Tests for zip to stream 203 | //============================================================================= 204 | TEST(ZipperMemoryOps, ZipToStream) 205 | { 206 | std::stringstream zipDataStream; 207 | const std::string entryName = "stream_entry.txt"; 208 | const std::string content = "stream content"; 209 | 210 | std::stringstream inputStream2("append content"); 211 | const std::string appendEntryName = "append_entry.txt"; 212 | 213 | // Constructor for stream 214 | { 215 | Zipper zipper(zipDataStream, Zipper::OpenFlags::Overwrite); 216 | std::stringstream inputStream(content); 217 | ASSERT_TRUE(zipper.add(inputStream, entryName)); 218 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 219 | zipper.close(); 220 | } 221 | 222 | // Check stream content exists 223 | zipDataStream.seekg(0, std::ios::end); 224 | ASSERT_GT(zipDataStream.tellg(), 0); 225 | zipDataStream.seekg(0, std::ios::beg); // Reset for unzipper 226 | 227 | // Append to stream 228 | { 229 | Zipper zipper(zipDataStream, Zipper::OpenFlags::Append); 230 | ASSERT_TRUE(zipper.add(inputStream2, appendEntryName)); 231 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 232 | zipper.close(); 233 | } 234 | 235 | // Reopen the stream 236 | { 237 | Zipper zipper; 238 | zipper.open(zipDataStream, "password", Zipper::OpenFlags::Append); 239 | ASSERT_TRUE(zipper.add(inputStream2, appendEntryName)); 240 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 241 | zipper.close(); 242 | } 243 | 244 | // Verify content using Unzipper 245 | { 246 | Unzipper unzipper(zipDataStream); 247 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 248 | auto entries = unzipper.entries(); 249 | ASSERT_EQ(entries.size(), 3); 250 | ASSERT_EQ(entries[0].name, entryName); 251 | ASSERT_EQ(entries[1].name, appendEntryName); 252 | ASSERT_EQ(entries[2].name, appendEntryName); 253 | 254 | std::stringstream extractedStream; 255 | ASSERT_TRUE(unzipper.extract(entryName, extractedStream)); 256 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 257 | ASSERT_EQ(extractedStream.str(), content); 258 | unzipper.close(); 259 | } 260 | 261 | // Test with empty stream 262 | { 263 | Zipper zipper(zipDataStream, Zipper::OpenFlags::Overwrite); 264 | zipper.close(); 265 | 266 | Unzipper unzipper(zipDataStream); 267 | auto entries = unzipper.entries(); 268 | ASSERT_EQ(entries.size(), 0); 269 | unzipper.close(); 270 | } 271 | 272 | // Test open() with stream and password 273 | { 274 | Zipper zipper; 275 | ASSERT_TRUE(zipper.open(zipDataStream, "test_password")); 276 | std::stringstream inputStream("password protected content"); 277 | ASSERT_TRUE(zipper.add(inputStream, "password_entry.txt")); 278 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 279 | zipper.close(); 280 | 281 | // Verify with Unzipper using password 282 | Unzipper unzipper(zipDataStream, "test_password"); 283 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 284 | auto entries = unzipper.entries(); 285 | ASSERT_EQ(entries.size(), 1); 286 | ASSERT_EQ(entries[0].name, "password_entry.txt"); 287 | unzipper.close(); 288 | } 289 | 290 | // Test open() with stream, password and flags 291 | { 292 | Zipper zipper; 293 | ASSERT_TRUE(zipper.open( 294 | zipDataStream, "test_password2", Zipper::OpenFlags::Append)); 295 | std::stringstream inputStream("another password protected content"); 296 | ASSERT_TRUE(zipper.add(inputStream, "password_entry2.txt")); 297 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 298 | zipper.close(); 299 | 300 | // Verify with Unzipper using password 301 | Unzipper unzipper(zipDataStream, "test_password2"); 302 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 303 | auto entries = unzipper.entries(); 304 | ASSERT_EQ(entries.size(), 2); // Should have both password entries 305 | unzipper.close(); 306 | } 307 | 308 | // Test open() with stream and flags (no password) 309 | { 310 | Zipper zipper; 311 | ASSERT_TRUE( 312 | zipper.open(zipDataStream, "", Zipper::OpenFlags::Overwrite)); 313 | std::stringstream inputStream("content without password"); 314 | ASSERT_TRUE(zipper.add(inputStream, "no_password_entry.txt")); 315 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 316 | zipper.close(); 317 | 318 | // Verify with Unzipper (no password needed) 319 | Unzipper unzipper(zipDataStream); 320 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 321 | auto entries = unzipper.entries(); 322 | ASSERT_EQ(entries.size(), 1); 323 | ASSERT_EQ(entries[0].name, "no_password_entry.txt"); 324 | unzipper.close(); 325 | } 326 | } 327 | 328 | //============================================================================= 329 | // Tests for multiple open sequences 330 | //============================================================================= 331 | TEST(ZipperMemoryOps, MultipleOpenSequence) 332 | { 333 | const std::string zipFileName = "test_multiple_open.zip"; 334 | const std::string entryName = "test_entry.txt"; 335 | const std::string content = "test content for multiple open sequence"; 336 | 337 | // Create initial zip file 338 | { 339 | Zipper zipper(zipFileName); 340 | std::stringstream contentStream(content); 341 | ASSERT_TRUE(zipper.add(contentStream, entryName)); 342 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 343 | zipper.close(); 344 | } 345 | 346 | // Test sequence: file -> vector -> stream 347 | { 348 | // 1. Open from file 349 | Unzipper unzipper(zipFileName); 350 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 351 | std::vector data1; 352 | ASSERT_TRUE(unzipper.extract(entryName, data1)); 353 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 354 | std::string extracted1(data1.begin(), data1.end()); 355 | ASSERT_EQ(extracted1, content); 356 | unzipper.close(); 357 | 358 | // 2. Open from vector 359 | std::vector zipData; 360 | { 361 | std::ifstream ifs(zipFileName, std::ios::binary); 362 | ASSERT_TRUE(ifs.is_open()); 363 | zipData = std::vector( 364 | (std::istreambuf_iterator(ifs)), 365 | std::istreambuf_iterator()); 366 | } 367 | ASSERT_FALSE(zipData.empty()); 368 | 369 | Unzipper unzipper2(zipData); 370 | ASSERT_FALSE(unzipper2.error()) << unzipper2.error().message(); 371 | std::vector data2; 372 | ASSERT_TRUE(unzipper2.extract(entryName, data2)); 373 | ASSERT_FALSE(unzipper2.error()) << unzipper2.error().message(); 374 | std::string extracted2(data2.begin(), data2.end()); 375 | ASSERT_EQ(extracted2, content); 376 | unzipper2.close(); 377 | 378 | // 3. Open from stream 379 | std::stringstream zipStream; 380 | zipStream.write(reinterpret_cast(zipData.data()), 381 | std::streamsize(zipData.size())); 382 | zipStream.seekg(0); 383 | 384 | Unzipper unzipper3(zipStream); 385 | ASSERT_FALSE(unzipper3.error()) << unzipper3.error().message(); 386 | std::vector data3; 387 | ASSERT_TRUE(unzipper3.extract(entryName, data3)); 388 | ASSERT_FALSE(unzipper3.error()) << unzipper3.error().message(); 389 | std::string extracted3(data3.begin(), data3.end()); 390 | ASSERT_EQ(extracted3, content); 391 | unzipper3.close(); 392 | } 393 | 394 | // Cleanup 395 | ASSERT_TRUE(helper::removeFileOrDir(zipFileName)); 396 | } 397 | 398 | //============================================================================= 399 | // Tests for multiple open with same Zipper instance 400 | //============================================================================= 401 | TEST(ZipperMemoryOps, MultipleOpenWithSameZipper) 402 | { 403 | const std::string zipFileName = "test_multiple_open_same.zip"; 404 | const std::string entryName1 = "test_entry1.txt"; 405 | const std::string entryName2 = "test_entry2.txt"; 406 | const std::string entryName3 = "test_entry3.txt"; 407 | const std::string content1 = "test content for file"; 408 | const std::string content2 = "test content for vector"; 409 | const std::string content3 = "test content for stream"; 410 | 411 | // Create Zipper instance 412 | Zipper zipper(zipFileName); 413 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 414 | 415 | // 1. Add content to file 416 | { 417 | std::stringstream contentStream(content1); 418 | ASSERT_TRUE(zipper.add(contentStream, entryName1)); 419 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 420 | zipper.close(); 421 | } 422 | 423 | // Verify file content 424 | { 425 | Unzipper unzipper(zipFileName); 426 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 427 | std::vector data; 428 | ASSERT_TRUE(unzipper.extract(entryName1, data)); 429 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 430 | std::string extracted(data.begin(), data.end()); 431 | ASSERT_EQ(extracted, content1); 432 | unzipper.close(); 433 | } 434 | 435 | // 2. Open with vector and add content 436 | std::vector zipData; 437 | { 438 | ASSERT_TRUE(zipper.open(zipData)); 439 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 440 | 441 | std::stringstream contentStream(content2); 442 | ASSERT_TRUE(zipper.add(contentStream, entryName2)); 443 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 444 | zipper.close(); 445 | } 446 | 447 | // Verify vector content 448 | { 449 | Unzipper unzipper(zipData); 450 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 451 | std::vector data; 452 | ASSERT_TRUE(unzipper.extract(entryName2, data)); 453 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 454 | std::string extracted(data.begin(), data.end()); 455 | ASSERT_EQ(extracted, content2); 456 | unzipper.close(); 457 | } 458 | 459 | // 3. Open with stream and add content 460 | std::stringstream zipStream; 461 | { 462 | ASSERT_TRUE(zipper.open(zipStream)); 463 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 464 | 465 | std::stringstream contentStream(content3); 466 | ASSERT_TRUE(zipper.add(contentStream, entryName3)); 467 | ASSERT_FALSE(zipper.error()) << zipper.error().message(); 468 | zipper.close(); 469 | } 470 | 471 | // Verify stream content 472 | { 473 | zipStream.seekg(0); 474 | Unzipper unzipper(zipStream); 475 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 476 | std::vector data; 477 | ASSERT_TRUE(unzipper.extract(entryName3, data)); 478 | ASSERT_FALSE(unzipper.error()) << unzipper.error().message(); 479 | std::string extracted(data.begin(), data.end()); 480 | ASSERT_EQ(extracted, content3); 481 | unzipper.close(); 482 | } 483 | 484 | // Cleanup 485 | ASSERT_TRUE(helper::removeFileOrDir(zipFileName)); 486 | } 487 | 488 | //============================================================================= 489 | // Tests for adding file with timestamp 490 | //============================================================================= 491 | TEST(ZipperMemoryOps, AddFileWithTimestamp) 492 | { 493 | helper::createFile("somefile.txt", "some content"); 494 | std::ifstream input("somefile.txt"); 495 | 496 | std::tm timestamp; 497 | timestamp.tm_year = 2024; 498 | timestamp.tm_mon = 0; // January (0-11) 499 | timestamp.tm_mday = 1; // 1st day 500 | timestamp.tm_hour = 12; // 12:00 501 | timestamp.tm_min = 1; 502 | timestamp.tm_sec = 2; 503 | 504 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 505 | zipper.add(input, timestamp, "somefile.txt"); 506 | zipper.close(); 507 | input.close(); 508 | 509 | Unzipper unzipper("ziptest.zip"); 510 | auto entries = unzipper.entries(); 511 | unzipper.close(); 512 | 513 | ASSERT_EQ(entries.size(), 1); 514 | 515 | ZipEntry& entry = entries[0]; 516 | ASSERT_EQ(entry.name, "somefile.txt"); 517 | ASSERT_STREQ(entry.timestamp.c_str(), "2024-0-1 12:1:2"); 518 | ASSERT_EQ(entry.unix_date.tm_year, timestamp.tm_year); 519 | ASSERT_EQ(entry.unix_date.tm_mon, timestamp.tm_mon); 520 | ASSERT_EQ(entry.unix_date.tm_mday, timestamp.tm_mday); 521 | ASSERT_EQ(entry.unix_date.tm_hour, timestamp.tm_hour); 522 | ASSERT_EQ(entry.unix_date.tm_min, timestamp.tm_min); 523 | ASSERT_EQ(entry.unix_date.tm_sec, timestamp.tm_sec); 524 | ASSERT_NE(entry.compressed_size, 0); 525 | ASSERT_NE(entry.uncompressed_size, 0); 526 | 527 | ASSERT_TRUE(helper::removeFileOrDir("somefile.txt")); 528 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 529 | } -------------------------------------------------------------------------------- /include/Zipper/Unzipper.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | //----------------------------------------------------------------------------- 7 | 8 | #ifndef ZIPPER_UNZIPPER_HPP 9 | #define ZIPPER_UNZIPPER_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | // ***************************************************************************** 22 | // Generated by CMake. Needed for exporting symbols for making the libzipper.dll 23 | // on Windows. 24 | // ***************************************************************************** 25 | #if defined(ZIPPER_EXPORT_DEFINED) 26 | # include "zipper_export.h" 27 | #else 28 | # define ZIPPER_EXPORT 29 | #endif 30 | 31 | namespace zipper 32 | { 33 | 34 | class ZipEntry; 35 | 36 | // ***************************************************************************** 37 | //! \brief Zip archive extractor/decompressor. 38 | // ***************************************************************************** 39 | class ZIPPER_EXPORT Unzipper 40 | { 41 | public: 42 | 43 | // ------------------------------------------------------------------------- 44 | //! \brief Overwrite mode for extraction operations. 45 | // ------------------------------------------------------------------------- 46 | enum class OverwriteMode 47 | { 48 | DoNotOverwrite, //!< Do not overwrite existing files 49 | Overwrite //!< Overwrite existing files 50 | }; 51 | 52 | // ------------------------------------------------------------------------- 53 | //! \brief Structure containing progress information during extraction. 54 | // ------------------------------------------------------------------------- 55 | struct Progress 56 | { 57 | //! \brief Status of the extraction 58 | enum class Status 59 | { 60 | OK, //!< Extraction completed successfully 61 | KO, //!< Extraction failed 62 | InProgress //!< Extraction in progress 63 | }; 64 | 65 | void reset() 66 | { 67 | status = Status::InProgress; 68 | current_file.clear(); 69 | bytes_read = 0; 70 | total_bytes = 0; 71 | files_extracted = 0; 72 | total_files = 1; 73 | } 74 | 75 | Status status = Status::InProgress; //!< Current status 76 | std::string current_file; //!< Name of the file being extracted 77 | uint64_t bytes_read = 0; //!< Number of bytes read so far 78 | uint64_t total_bytes = 0; //!< Total number of bytes to extract 79 | size_t files_extracted = 0; //!< Number of files extracted so far 80 | size_t total_files = 0; //!< Total number of files to extract 81 | }; 82 | 83 | // ------------------------------------------------------------------------- 84 | //! \brief Callback type for progress reporting 85 | //! \param[in] progress Current progress information 86 | // ------------------------------------------------------------------------- 87 | using ProgressCallback = std::function; 88 | 89 | // ------------------------------------------------------------------------- 90 | //! \brief Default constructor. Creates an uninitialized Unzipper. 91 | // ------------------------------------------------------------------------- 92 | Unzipper(); 93 | 94 | // ------------------------------------------------------------------------- 95 | //! \brief Regular zip decompressor (from zip archive file). 96 | //! 97 | //! \param[in] p_zip_name Path of the zip file to extract. 98 | //! \param[in] p_password Optional password used during compression (empty 99 | //! if no password). 100 | //! \throw std::runtime_error if an error occurs during initialization. 101 | // ------------------------------------------------------------------------- 102 | Unzipper(std::string const& p_zip_name, 103 | std::string const& p_password = std::string()); 104 | 105 | // ------------------------------------------------------------------------- 106 | //! \brief In-memory zip decompressor (from std::iostream). 107 | //! 108 | //! \param[in,out] p_buffer Stream containing zipped entries to extract. 109 | //! \param[in] p_password Optional password used during compression (empty 110 | //! if no password). 111 | //! \throw std::runtime_error if an error occurs during initialization. 112 | // ------------------------------------------------------------------------- 113 | Unzipper(std::istream& p_buffer, 114 | std::string const& p_password = std::string()); 115 | 116 | // ------------------------------------------------------------------------- 117 | //! \brief In-memory zip decompressor (from std::vector). 118 | //! 119 | //! \param[in] p_buffer Vector containing zipped entries to extract. 120 | //! The vector content is copied internally. 121 | //! \param[in] p_password Optional password used during compression (empty 122 | //! if no password). 123 | //! \throw std::runtime_error if an error occurs during initialization. 124 | // ------------------------------------------------------------------------- 125 | Unzipper(const std::vector& p_buffer, 126 | std::string const& p_password = std::string()); 127 | 128 | // ------------------------------------------------------------------------- 129 | //! \brief Calls release() and close() methods. 130 | // ------------------------------------------------------------------------- 131 | ~Unzipper(); 132 | 133 | // ------------------------------------------------------------------------- 134 | //! \brief Regular zip decompressor (from zip archive file). 135 | //! 136 | //! \param[in] p_zip_name Path of the zip file to extract. 137 | //! \param[in] p_password Optional password used during compression (empty 138 | //! if no password). 139 | //! \throw std::runtime_error if an error occurs during initialization. 140 | // ------------------------------------------------------------------------- 141 | bool open(std::string const& p_zip_name, 142 | std::string const& p_password = std::string()); 143 | 144 | // ------------------------------------------------------------------------- 145 | //! \brief In-memory zip decompressor (from std::iostream). 146 | //! 147 | //! \param[in,out] p_buffer Stream containing zipped entries to extract. 148 | //! \param[in] p_password Optional password used during compression (empty 149 | //! if no password). 150 | //! \throw std::runtime_error if an error occurs during initialization. 151 | // ------------------------------------------------------------------------- 152 | bool open(std::istream& p_buffer, 153 | std::string const& p_password = std::string()); 154 | 155 | // ------------------------------------------------------------------------- 156 | //! \brief In-memory zip decompressor (from std::vector). 157 | //! 158 | //! \param[in] p_buffer Vector containing zipped entries to extract. 159 | //! The vector content is copied internally. 160 | //! \param[in] p_password Optional password used during compression (empty 161 | //! if no password). 162 | //! \throw std::runtime_error if an error occurs during initialization. 163 | // ------------------------------------------------------------------------- 164 | bool open(const std::vector& p_buffer, 165 | std::string const& p_password = std::string()); 166 | 167 | // ------------------------------------------------------------------------- 168 | //! \brief Returns all entries contained in the zip archive. 169 | //! \note If no entries are found, the vector is empty but this may come 170 | //! from an internal error. Call error() to get to distinguish between 171 | //! an empty vector and an internal error. 172 | //! \return Vector of ZipEntry objects. 173 | // ------------------------------------------------------------------------- 174 | std::vector entries(); 175 | 176 | // ------------------------------------------------------------------------- 177 | //! \brief Returns all entries contained in the zip archive matching the 178 | //! glob pattern. 179 | //! \note If no entries are found, the vector is empty but this may come 180 | //! from an internal error. Call error() to get to distinguish between 181 | //! an empty vector and an internal error. 182 | //! \return Vector of ZipEntry objects. 183 | // ------------------------------------------------------------------------- 184 | std::vector entries(std::string const& p_glob_pattern); 185 | 186 | // ------------------------------------------------------------------------- 187 | //! \brief Returns the total uncompressed size of all entries in the zip 188 | //! archive. 189 | //! \note Call this method before extractAll() to check if the total 190 | //! uncompressed size is too large. Prevent zip bomb attacks. 191 | //! \return Total uncompressed size in bytes. 192 | // ------------------------------------------------------------------------- 193 | size_t sizeOnDisk(); 194 | 195 | // ------------------------------------------------------------------------- 196 | //! \brief Set the progress callback. 197 | //! \param[in] callback Function to call with progress updates. 198 | //! \return true if the callback was set, false otherwise. 199 | // ------------------------------------------------------------------------- 200 | bool setProgressCallback(ProgressCallback callback); 201 | 202 | // ------------------------------------------------------------------------- 203 | //! \brief Extract the whole zip archive using alternative destination names 204 | //! for existing files on the disk. 205 | //! 206 | //! \param[in] p_folder_destination Full path where files will be extracted 207 | //! (if empty, extracts to same folder as the zip file). 208 | //! \param[in] p_alternative_names Dictionary of alternative names for 209 | //! existing files (key: zip entry name, value: desired path name on disk). 210 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 211 | //! \return true on success, false on failure. Call error() for more info. 212 | // ------------------------------------------------------------------------- 213 | bool 214 | extractAll(std::string const& p_folder_destination, 215 | const std::map& p_alternative_names, 216 | OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 217 | 218 | // ------------------------------------------------------------------------- 219 | //! \brief Extract the whole archive to the desired disk destination. 220 | //! 221 | //! \param[in] p_folder_destination Full path where files will be extracted 222 | //! (if empty, extracts to same folder as the zip file). 223 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 224 | //! \return true on success, false on failure. Call error() for more info. 225 | // ------------------------------------------------------------------------- 226 | bool extractAll(std::string const& p_folder_destination, 227 | OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 228 | 229 | // ------------------------------------------------------------------------- 230 | //! \brief Extract the whole archive to the same folder as the zip file. 231 | //! 232 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 233 | //! \return true on success, false on failure. Call error() for more info. 234 | // ------------------------------------------------------------------------- 235 | bool extractAll(OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 236 | 237 | // ------------------------------------------------------------------------- 238 | //! \brief Extract entries matching a glob pattern. 239 | //! \note Glob pattern is the regular expression syntax used by the Unix 240 | //! shell. 241 | //! 242 | //! \param[in] p_glob Glob pattern to select entries. 243 | //! \param[in] p_folder_destination Full path where files will be extracted 244 | //! (if empty, extracts to same folder as the zip file). 245 | //! \param[in] p_alternative_names Dictionary of alternative names for 246 | //! existing files (key: zip entry name, value: desired path name on disk). 247 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 248 | //! \return true on success, false on failure. Call error() for more info. 249 | // ------------------------------------------------------------------------- 250 | bool 251 | extractGlob(std::string const& p_glob, 252 | std::string const& p_destination, 253 | const std::map& p_alternative_names, 254 | OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 255 | 256 | // ------------------------------------------------------------------------- 257 | //! \brief Extract entries matching a glob pattern. 258 | //! \note Glob pattern is the regular expression syntax used by the Unix 259 | //! shell. 260 | //! 261 | //! \param[in] p_glob Glob pattern to select entries. 262 | //! \param[in] p_folder_destination Full path where files will be extracted 263 | //! (if empty, extracts to same folder as the zip file). 264 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 265 | //! \return true on success, false on failure. Call error() for more info. 266 | // ------------------------------------------------------------------------- 267 | bool extractGlob(std::string const& p_glob, 268 | std::string const& p_folder_destination, 269 | OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 270 | 271 | // ------------------------------------------------------------------------- 272 | //! \brief Extract entries matching a glob pattern. 273 | //! \note Glob pattern is the regular expression syntax used by the Unix 274 | //! shell. 275 | //! 276 | //! \param[in] p_glob Glob pattern to select entries. 277 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 278 | //! \return true on success, false on failure. Call error() for more info. 279 | // ------------------------------------------------------------------------- 280 | bool extractGlob(std::string const& p_glob, 281 | OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 282 | 283 | // ------------------------------------------------------------------------- 284 | //! \brief Extract a single entry from the archive. 285 | //! 286 | //! \param[in] p_entry_name Entry path inside the zip archive. 287 | //! \param[in] p_entry_destination Full path where the file will be 288 | //! extracted. 289 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 290 | //! \return true on success, false on failure. 291 | //! Call error() for more info. 292 | // ------------------------------------------------------------------------- 293 | bool extract(std::string const& p_entry_name, 294 | std::string const& p_entry_destination, 295 | OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 296 | 297 | // ------------------------------------------------------------------------- 298 | //! \brief Extract a single entry to the same folder as the zip file. 299 | //! 300 | //! \param[in] p_entry_name Entry path inside the zip archive. 301 | //! \param[in] p_overwrite Overwrite mode for extraction operations. 302 | //! \return true on success, false on failure. Call error() for more info. 303 | // ------------------------------------------------------------------------- 304 | bool extract(std::string const& p_entry_name, 305 | OverwriteMode p_overwrite = OverwriteMode::DoNotOverwrite); 306 | 307 | // ------------------------------------------------------------------------- 308 | //! \brief Extract a single entry from zip to memory (stream). 309 | //! 310 | //! \param[in] p_entry_name Entry path inside the zip archive. 311 | //! \param[out] p_output_stream Stream that will receive the extracted entry 312 | //! data. 313 | //! \return true on success, false on failure. Call error() for more info. 314 | // ------------------------------------------------------------------------- 315 | bool extract(std::string const& p_entry_name, 316 | std::ostream& p_output_stream); 317 | 318 | // ------------------------------------------------------------------------- 319 | //! \brief Extract a single entry from zip to memory (vector). 320 | //! 321 | //! \param[in] p_entry_name Entry path inside the zip archive. 322 | //! \param[out] p_output_buffer Vector that will receive the extracted entry 323 | //! data. 324 | //! \return true on success, false on failure. Call error() for more info. 325 | // ------------------------------------------------------------------------- 326 | bool extract(std::string const& p_entry_name, 327 | std::vector& p_output_buffer); 328 | 329 | // ------------------------------------------------------------------------- 330 | //! \brief Closes the archive. Called by the destructor. 331 | // ------------------------------------------------------------------------- 332 | void close(); 333 | 334 | // ------------------------------------------------------------------------- 335 | //! \brief Get the error information when a method returned false. 336 | //! \return Reference to the error code. 337 | // ------------------------------------------------------------------------- 338 | inline std::error_code const& error() const 339 | { 340 | return m_error_code; 341 | } 342 | 343 | // ------------------------------------------------------------------------- 344 | //! \brief Check if the unzipper is currently open and ready for extracting 345 | //! files. 346 | //! \return True if the unzipper is open, false otherwise. 347 | // ------------------------------------------------------------------------- 348 | inline bool isOpened() const 349 | { 350 | return m_open; 351 | } 352 | 353 | private: 354 | 355 | // ------------------------------------------------------------------------- 356 | //! \brief Check if the unzipper is valid. 357 | //! \return true if the unzipper is valid, false otherwise. 358 | // ------------------------------------------------------------------------- 359 | bool checkValid(); 360 | 361 | private: 362 | 363 | struct Impl; 364 | 365 | //! \brief Whether the archive is open. 366 | bool m_open = false; 367 | //! \brief Error code. 368 | std::error_code m_error_code; 369 | //! \brief Implementation. 370 | std::unique_ptr m_impl; 371 | }; 372 | 373 | // ************************************************************************* 374 | //! \brief Class representing an entry in a zip archive. 375 | // ************************************************************************* 376 | class ZipEntry 377 | { 378 | public: 379 | 380 | //! \brief Default constructor. 381 | ZipEntry() = default; 382 | 383 | //! \brief Constructor with entry details. 384 | //! 385 | //! \param[in] p_name Name of the entry in the zip archive. 386 | //! \param[in] p_compressed_size Size of the compressed data in bytes. 387 | //! \param[in] p_uncompressed_size Original size of the data in bytes. 388 | //! \param[in] p_year Year component of the timestamp. 389 | //! \param[in] p_month Month component of the timestamp (1-12). 390 | //! \param[in] p_day Day component of the timestamp (1-31). 391 | //! \param[in] p_hour Hour component of the timestamp (0-23). 392 | //! \param[in] p_minute Minute component of the timestamp (0-59). 393 | //! \param[in] p_second Second component of the timestamp (0-59). 394 | //! \param[in] p_dos_date DOS-format date. 395 | ZipEntry(std::string const& p_name, 396 | uint64_t p_compressed_size, 397 | uint64_t p_uncompressed_size, 398 | uint32_t p_year, 399 | uint32_t p_month, 400 | uint32_t p_day, 401 | uint32_t p_hour, 402 | uint32_t p_minute, 403 | uint32_t p_second, 404 | uint32_t p_dos_date) 405 | : name(p_name), 406 | compressed_size(p_compressed_size), 407 | uncompressed_size(p_uncompressed_size), 408 | dos_date(p_dos_date) 409 | { 410 | // timestamp YYYY-MM-DD HH:MM:SS 411 | std::stringstream str; 412 | str << p_year << "-" << p_month << "-" << p_day << " " << p_hour << ":" 413 | << p_minute << ":" << p_second; 414 | timestamp = str.str(); 415 | 416 | unix_date.tm_year = p_year; 417 | unix_date.tm_mon = p_month; 418 | unix_date.tm_mday = p_day; 419 | unix_date.tm_hour = p_hour; 420 | unix_date.tm_min = p_minute; 421 | unix_date.tm_sec = p_second; 422 | } 423 | 424 | //! \brief Copy constructor. 425 | ZipEntry(ZipEntry const& p_other) = default; 426 | 427 | //! \brief Copy assignment operator. 428 | ZipEntry& operator=(ZipEntry const& p_other) = default; 429 | 430 | //! \brief Move assignment operator. 431 | ZipEntry& operator=(ZipEntry&& p_other) = default; 432 | 433 | public: 434 | 435 | //! \brief Structure representing a date and time. 436 | struct tm_s 437 | { 438 | uint32_t tm_sec; //!< Seconds (0-59) 439 | uint32_t tm_min; //!< Minutes (0-59) 440 | uint32_t tm_hour; //!< Hours (0-23) 441 | uint32_t tm_mday; //!< Day of month (1-31) 442 | uint32_t tm_mon; //!< Month (1-12) 443 | uint32_t tm_year; //!< Year (full year, e.g. 2022) 444 | }; 445 | 446 | std::string name; //!< Name of the entry in the zip archive 447 | std::string timestamp; //!< Formatted timestamp string (YYYY-MM-DD HH:MM:SS) 448 | uint64_t compressed_size; //!< Size of the compressed data in bytes 449 | uint64_t uncompressed_size; //!< Original size of the data in bytes 450 | uint32_t dos_date; //!< DOS-format date 451 | tm_s unix_date; //!< UNIX-format date and time 452 | }; 453 | 454 | } // namespace zipper 455 | 456 | #endif // ZIPPER_UNZIPPER_HPP 457 | -------------------------------------------------------------------------------- /tests/TestIssues.cpp: -------------------------------------------------------------------------------- 1 | #include "gmock/gmock.h" 2 | #include "gtest/gtest.h" 3 | 4 | #define protected public 5 | #define private public 6 | #include "Zipper/Unzipper.hpp" 7 | #include "Zipper/Zipper.hpp" 8 | #undef protected 9 | #undef private 10 | 11 | #include "TestHelper.hpp" 12 | 13 | using namespace zipper; 14 | 15 | #ifndef PWD 16 | # error "PWD shall be defined" 17 | #endif 18 | 19 | //============================================================================= 20 | // https://github.com/Lecrapouille/zipper/issues/5 21 | //============================================================================= 22 | TEST(MemoryZipTests, Issue05_1) 23 | { 24 | zipper::Unzipper unzipper(PWD "/issues/issue_05_1.zip"); 25 | std::string temp_dir = "issue_05_1/"; 26 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 27 | 28 | // Check the zip file given in the GitHub issue 29 | std::vector entries = unzipper.entries(); 30 | ASSERT_EQ(entries.size(), 3u); 31 | ASSERT_STREQ(entries[0].name.c_str(), "sim.sedml"); 32 | ASSERT_STREQ(entries[1].name.c_str(), "model.xml"); 33 | ASSERT_STREQ(entries[2].name.c_str(), "manifest.xml"); 34 | 35 | // Check can be extracted 36 | ASSERT_TRUE( 37 | unzipper.extractAll(temp_dir, Unzipper::OverwriteMode::DoNotOverwrite)); 38 | ASSERT_TRUE(helper::checkFileExists(temp_dir + "sim.sedml", "")); 39 | ASSERT_TRUE(helper::checkFileExists(temp_dir + "model.xml", "")); 40 | ASSERT_TRUE(helper::checkFileExists(temp_dir + "manifest.xml")); 41 | 42 | // Check cannot be extracted a second time, by security: files already 43 | // exist, if flag is set. 44 | std::string error = "Security Error: '" + 45 | Path::toNativeSeparators(temp_dir + "manifest.xml") + 46 | "' already exists and would have been replaced!"; 47 | ASSERT_FALSE( 48 | unzipper.extractAll(temp_dir, Unzipper::OverwriteMode::DoNotOverwrite)); 49 | ASSERT_STREQ(unzipper.error().message().c_str(), error.c_str()); 50 | 51 | // Check cannot be extracted, by security: files already exist. 52 | ASSERT_FALSE(unzipper.extractAll(temp_dir)); // Implicit: false argument 53 | ASSERT_STREQ(unzipper.error().message().c_str(), error.c_str()); 54 | 55 | // Check can be extracted, when security is disabled. 56 | ASSERT_TRUE( 57 | unzipper.extractAll(temp_dir, Unzipper::OverwriteMode::Overwrite)); 58 | ASSERT_TRUE(helper::checkFileExists(temp_dir + "sim.sedml", "")); 59 | ASSERT_TRUE(helper::checkFileExists(temp_dir + "model.xml", "")); 60 | ASSERT_TRUE(helper::checkFileExists(temp_dir + "manifest.xml")); 61 | 62 | unzipper.close(); 63 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir)); 64 | } 65 | 66 | //============================================================================= 67 | // https://github.com/Lecrapouille/zipper/issues/5 68 | //============================================================================= 69 | TEST(MemoryZipTests, Issue05_nopassword) 70 | { 71 | zipper::Unzipper unzipper(PWD "/issues/issue_05_nopassword.zip"); 72 | ASSERT_TRUE(helper::removeFileOrDir("issue_05")); 73 | 74 | // Check the zip file given in the GitHub issue 75 | std::vector entries = unzipper.entries(); 76 | ASSERT_EQ(entries.size(), 5u); 77 | ASSERT_STREQ(entries[0].name.c_str(), "issue_05/"); 78 | ASSERT_STREQ(entries[1].name.c_str(), "issue_05/Nouveau dossier/"); 79 | ASSERT_STREQ(entries[2].name.c_str(), "issue_05/Nouveau fichier vide"); 80 | ASSERT_STREQ(entries[3].name.c_str(), "issue_05/foo/"); 81 | ASSERT_STREQ(entries[4].name.c_str(), "issue_05/foo/bar"); 82 | 83 | // Check can be extracted 84 | ASSERT_TRUE( 85 | unzipper.extractAll(".", Unzipper::OverwriteMode::DoNotOverwrite)); 86 | unzipper.close(); 87 | 88 | // Check files and directories were created 89 | ASSERT_TRUE(helper::checkDirExists("issue_05/")); 90 | ASSERT_TRUE(helper::checkDirExists("issue_05/Nouveau dossier/")); 91 | ASSERT_TRUE(helper::checkFileExists("issue_05/Nouveau fichier vide", "")); 92 | ASSERT_TRUE(helper::checkDirExists("issue_05/foo/")); 93 | ASSERT_TRUE(helper::checkFileExists("issue_05/foo/bar", "")); 94 | 95 | ASSERT_TRUE(helper::removeFileOrDir("issue_05")); 96 | } 97 | 98 | //============================================================================= 99 | // https://github.com/Lecrapouille/zipper/issues/5 100 | //============================================================================= 101 | TEST(MemoryZipTests, Issue05_password) 102 | { 103 | zipper::Unzipper unzipper(PWD "/issues/issue_05_password.zip", "1234"); 104 | ASSERT_TRUE(helper::removeFileOrDir("issue_05")); 105 | 106 | // Check the zip file given in the GitHub issue 107 | std::vector entries = unzipper.entries(); 108 | ASSERT_EQ(entries.size(), 5u); 109 | ASSERT_STREQ(entries[0].name.c_str(), "issue_05/"); 110 | ASSERT_STREQ(entries[1].name.c_str(), "issue_05/Nouveau dossier/"); 111 | ASSERT_STREQ(entries[2].name.c_str(), "issue_05/Nouveau fichier vide"); 112 | ASSERT_STREQ(entries[3].name.c_str(), "issue_05/foo/"); 113 | ASSERT_STREQ(entries[4].name.c_str(), "issue_05/foo/bar"); 114 | 115 | // Check can be extracted 116 | ASSERT_TRUE( 117 | unzipper.extractAll(".", Unzipper::OverwriteMode::DoNotOverwrite)); 118 | unzipper.close(); 119 | 120 | // Check files and directories were created 121 | ASSERT_TRUE(helper::checkDirExists("issue_05/")); 122 | ASSERT_TRUE(helper::checkDirExists("issue_05/Nouveau dossier/")); 123 | ASSERT_TRUE(helper::checkFileExists("issue_05/Nouveau fichier vide", "")); 124 | ASSERT_TRUE(helper::checkDirExists("issue_05/foo/")); 125 | ASSERT_TRUE(helper::checkFileExists("issue_05/foo/bar", "")); 126 | 127 | ASSERT_TRUE(helper::removeFileOrDir("issue_05")); 128 | } 129 | 130 | //============================================================================= 131 | // Tests for issue #21: Problems with directory paths ending with '/' 132 | // https://github.com/sebastiandev/zipper/issues/21 133 | //============================================================================= 134 | TEST(ZipTests, Issue21) 135 | { 136 | // Create folder 137 | ASSERT_TRUE(helper::removeFileOrDir("data")); 138 | ASSERT_TRUE(helper::createDir("data/somefolder/")); 139 | ASSERT_TRUE(helper::createFile("data/somefolder/test.txt", 140 | "test file2 compression")); 141 | 142 | // Test with the '/' with and without the option Zipper::SaveHierarchy 143 | { 144 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 145 | EXPECT_EQ(zipper.add("data/somefolder/", Zipper::SaveHierarchy), true); 146 | EXPECT_EQ(zipper.add("data/somefolder/", Zipper::SaveHierarchy), true); 147 | EXPECT_EQ(zipper.add("data/somefolder/"), true); 148 | EXPECT_EQ(zipper.add("data/somefolder/"), true); 149 | zipper.close(); 150 | 151 | // Verify entries were added successfully 152 | zipper::Unzipper unzipper("ziptest.zip"); 153 | EXPECT_EQ(unzipper.entries().size(), 4u); 154 | EXPECT_EQ(unzipper.entries()[0].name, "data/somefolder/test.txt"); 155 | EXPECT_EQ(unzipper.entries()[1].name, "data/somefolder/test.txt"); 156 | EXPECT_EQ(unzipper.entries()[2].name, "test.txt"); 157 | EXPECT_EQ(unzipper.entries()[3].name, "test.txt"); 158 | unzipper.close(); 159 | 160 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 161 | ASSERT_TRUE(helper::removeFileOrDir("data")); 162 | } 163 | 164 | // Test without the '/' with and without the option Zipper::SaveHierarchy 165 | { 166 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 167 | EXPECT_EQ(zipper.add("data/somefolder", Zipper::SaveHierarchy), false); 168 | ASSERT_THAT(zipper.error().message(), 169 | testing::HasSubstr("Failed opening file")); 170 | EXPECT_EQ(zipper.add("data/somefolder", Zipper::SaveHierarchy), false); 171 | ASSERT_THAT(zipper.error().message(), 172 | testing::HasSubstr("Failed opening file")); 173 | EXPECT_EQ(zipper.add("data/somefolder"), false); 174 | ASSERT_THAT(zipper.error().message(), 175 | testing::HasSubstr("Failed opening file")); 176 | EXPECT_EQ(zipper.add("data/somefolder"), false); 177 | ASSERT_THAT(zipper.error().message(), 178 | testing::HasSubstr("Failed opening file")); 179 | zipper.close(); 180 | 181 | // Verify entries were added successfully 182 | zipper::Unzipper unzipper("ziptest.zip"); 183 | EXPECT_EQ(unzipper.entries().size(), 0u); 184 | unzipper.close(); 185 | 186 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 187 | ASSERT_TRUE(helper::removeFileOrDir("data")); 188 | } 189 | } 190 | 191 | //============================================================================= 192 | // Tests for issue #33: Security vulnerability - Zip Slip - Zipping 193 | // https://github.com/sebastiandev/zipper/issues/33 194 | //============================================================================= 195 | TEST(ZipTests, Issue33_zipping) 196 | { 197 | // Test with path traversal attack (using "../") is not allowed. 198 | { 199 | Path::remove("ziptest.zip"); 200 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 201 | EXPECT_FALSE( 202 | helper::zipAddFile(zipper, "Test1.txt", "hello", "../Test1")); 203 | ASSERT_THAT(zipper.error().message(), 204 | testing::HasSubstr("escape the destination directory")); 205 | zipper.close(); 206 | 207 | // Verify no entries were not added. 208 | Unzipper unzipper("ziptest.zip"); 209 | EXPECT_EQ(unzipper.entries().size(), 0u); 210 | unzipper.close(); 211 | 212 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 213 | } 214 | 215 | // Test with normalized path (foo/../Test1 -> Test1) is allowed. 216 | { 217 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 218 | EXPECT_TRUE( 219 | helper::zipAddFile(zipper, "Test1.txt", "world", "foo/../Test1")); 220 | zipper.close(); 221 | 222 | // Verify entries were added successfully. 223 | Unzipper unzipper("ziptest.zip"); 224 | EXPECT_EQ(unzipper.entries().size(), 1u); 225 | EXPECT_STREQ(unzipper.entries()[0].name.c_str(), "Test1"); 226 | unzipper.close(); 227 | 228 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 229 | } 230 | } 231 | 232 | //============================================================================= 233 | // Tests for issue #33: Security vulnerability - Zip Slip - Unzipping 234 | // https://github.com/sebastiandev/zipper/issues/33 235 | //============================================================================= 236 | TEST(ZipTests, Issue33_unzipping) 237 | { 238 | // Test with path traversal attack when extracting. Check that the file is 239 | // not created. 240 | { 241 | ASSERT_FALSE(Path::exist("../Test1")); 242 | 243 | Unzipper unzipper(PWD "/issues/issue33_1.zip"); 244 | EXPECT_EQ(unzipper.entries().size(), 1u); 245 | EXPECT_STREQ(unzipper.entries()[0].name.c_str(), "../Test1"); 246 | EXPECT_EQ(unzipper.extract("../Test1"), false); 247 | #if defined(_WIN32) 248 | EXPECT_STREQ(unzipper.error().message().c_str(), 249 | "Security error: entry '..\\Test1' would be outside your " 250 | "target directory"); 251 | #else 252 | EXPECT_STREQ(unzipper.error().message().c_str(), 253 | "Security error: entry '../Test1' would be outside your " 254 | "target directory"); 255 | #endif 256 | 257 | unzipper.close(); 258 | EXPECT_FALSE(Path::exist("../Test1")); 259 | } 260 | 261 | // Test with normalized path within zip (foo/../Test1 -> Test1). Check 262 | // that the file is created. 263 | { 264 | ASSERT_TRUE(helper::removeFileOrDir("Test1")); 265 | 266 | Unzipper unzipper(PWD "/issues/issue33_2.zip"); 267 | EXPECT_EQ(unzipper.entries().size(), 1u); 268 | EXPECT_STREQ(unzipper.entries()[0].name.c_str(), "foo/../Test1"); 269 | EXPECT_TRUE(unzipper.extract("foo/../Test1")); 270 | unzipper.close(); 271 | EXPECT_TRUE(Path::exist("Test1")); 272 | EXPECT_TRUE(Path::isFile("Test1")); 273 | EXPECT_STREQ(helper::readFileContent("Test1").c_str(), "hello"); 274 | 275 | ASSERT_TRUE(helper::removeFileOrDir("Test1")); 276 | } 277 | } 278 | 279 | //============================================================================= 280 | // Tests for issue #34: Empty directories in zip archives 281 | // https://github.com/sebastiandev/zipper/issues/34 282 | //============================================================================= 283 | TEST(ZipTests, Issue34) 284 | { 285 | zipper::Unzipper unzipper(PWD "/issues/issue34.zip"); 286 | EXPECT_EQ(unzipper.entries().size(), 13u); 287 | EXPECT_STREQ(unzipper.entries()[0].name.c_str(), "issue34/"); 288 | EXPECT_STREQ(unzipper.entries()[1].name.c_str(), "issue34/1/"); 289 | EXPECT_STREQ(unzipper.entries()[2].name.c_str(), "issue34/1/.dummy"); 290 | EXPECT_STREQ(unzipper.entries()[3].name.c_str(), "issue34/1/2/"); 291 | EXPECT_STREQ(unzipper.entries()[4].name.c_str(), "issue34/1/2/3/"); 292 | EXPECT_STREQ(unzipper.entries()[5].name.c_str(), "issue34/1/2/3/4/"); 293 | EXPECT_STREQ(unzipper.entries()[6].name.c_str(), "issue34/1/2/3_1/"); 294 | EXPECT_STREQ(unzipper.entries()[7].name.c_str(), "issue34/1/2/3_1/3.1.txt"); 295 | EXPECT_STREQ(unzipper.entries()[8].name.c_str(), "issue34/1/2/foobar.txt"); 296 | EXPECT_STREQ(unzipper.entries()[9].name.c_str(), "issue34/11/"); 297 | EXPECT_STREQ(unzipper.entries()[10].name.c_str(), "issue34/11/foo/"); 298 | EXPECT_STREQ(unzipper.entries()[11].name.c_str(), "issue34/11/foo/bar/"); 299 | EXPECT_STREQ(unzipper.entries()[12].name.c_str(), 300 | "issue34/11/foo/bar/here.txt"); 301 | 302 | // Test extracting to temp directory 303 | std::string temp_dir = Path::getTempDirectory(); 304 | ASSERT_TRUE(helper::removeFileOrDir(temp_dir + "issue34")); 305 | EXPECT_TRUE(unzipper.extractAll(temp_dir)); 306 | 307 | // Verify all directories and files were correctly extracted 308 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/")); 309 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/")); 310 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/.dummy")); 311 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/2/")); 312 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/2/3/")); 313 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/2/3/4/")); 314 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/2/3_1/")); 315 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/2/3_1/3.1.txt")); 316 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/1/2/foobar.txt")); 317 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/11/")); 318 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/11/foo/")); 319 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/11/foo/bar/")); 320 | EXPECT_TRUE(Path::exist(temp_dir + "issue34/11/foo/bar/here.txt")); 321 | 322 | // Verify directories and files have the correct type 323 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/")); 324 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/1/")); 325 | EXPECT_TRUE(Path::isFile(temp_dir + "issue34/1/.dummy")); 326 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/1/2/")); 327 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/1/2/3/")); 328 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/1/2/3/4/")); 329 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/1/2/3_1/")); 330 | EXPECT_TRUE(Path::isFile(temp_dir + "issue34/1/2/3_1/3.1.txt")); 331 | EXPECT_TRUE(Path::isFile(temp_dir + "issue34/1/2/foobar.txt")); 332 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/11/")); 333 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/11/foo/")); 334 | EXPECT_TRUE(Path::isDir(temp_dir + "issue34/11/foo/bar/")); 335 | EXPECT_TRUE(Path::isFile(temp_dir + "issue34/11/foo/bar/here.txt")); 336 | 337 | // Verify file contents 338 | std::string f1 = temp_dir + "issue34/1/2/3_1/3.1.txt"; 339 | EXPECT_STREQ(helper::readFileContent(f1.c_str()).c_str(), "3.1\n"); 340 | std::string f2 = temp_dir + "issue34/1/2/foobar.txt"; 341 | EXPECT_STREQ(helper::readFileContent(f2.c_str()).c_str(), "foobar.txt\n"); 342 | std::string f3 = temp_dir + "issue34/11/foo/bar/here.txt"; 343 | EXPECT_STREQ(helper::readFileContent(f3.c_str()).c_str(), ""); 344 | } 345 | 346 | #if !defined(_WIN32) 347 | //============================================================================= 348 | // Tests for issue #83: Error handling for non-existent paths 349 | // https://github.com/sebastiandev/zipper/issues/83 350 | //============================================================================= 351 | TEST(MemoryZipTests, Issue83) 352 | { 353 | std::string error; 354 | 355 | // Create folder with test file 356 | Path::remove("data"); 357 | Path::createDir("data/somefolder/"); 358 | ASSERT_TRUE(helper::createFile("data/somefolder/test.txt", 359 | "test file2 compression")); 360 | 361 | // Create zip file 362 | Path::remove("ziptest.zip"); 363 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 364 | EXPECT_TRUE(zipper.add("data/somefolder/", Zipper::SaveHierarchy)); 365 | zipper.close(); 366 | 367 | // Test extraction to various invalid paths 368 | zipper::Unzipper unzipper("ziptest.zip"); 369 | 370 | std::string does_not_exist = "/does/not/exist"; 371 | std::string no_permissions = "/usr/bin"; 372 | 373 | // Test extraction to non-existent path 374 | EXPECT_FALSE(unzipper.extract("data/somefolder/test.txt", does_not_exist)); 375 | ASSERT_THAT(unzipper.error().message(), 376 | testing::HasSubstr("Permission denied")); 377 | 378 | // Test extractAll to non-existent path 379 | EXPECT_FALSE(unzipper.extractAll(does_not_exist)); 380 | ASSERT_THAT(unzipper.error().message(), 381 | testing::HasSubstr("Failed creating folder")); 382 | 383 | // Test extraction to system path without permissions 384 | EXPECT_FALSE(unzipper.extract("data/somefolder/test.txt", no_permissions)); 385 | ASSERT_THAT(unzipper.error().message(), 386 | testing::HasSubstr("Permission denied")); 387 | 388 | // Test extractAll to system path without permissions 389 | EXPECT_FALSE(unzipper.extractAll(no_permissions)); 390 | ASSERT_THAT(unzipper.error().message(), 391 | testing::HasSubstr("Permission denied")); 392 | 393 | // Clean up 394 | ASSERT_TRUE(helper::removeFileOrDir("data")); 395 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 396 | } 397 | #endif 398 | 399 | //============================================================================= 400 | // Tests for issue #118: Empty zip file handling 401 | // https://github.com/sebastiandev/zipper/issues/118 402 | //============================================================================= 403 | TEST(MemoryZipTests, Issue118) 404 | { 405 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 406 | 407 | // Create a dummy zip file 408 | { 409 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 410 | zipper.close(); 411 | } 412 | 413 | // Check there is no entries in empty zip and that extraction fails 414 | { 415 | zipper::Unzipper unzipper("ziptest.zip"); 416 | EXPECT_EQ(unzipper.entries().size(), 0u); 417 | 418 | ASSERT_FALSE( 419 | unzipper.extractAll(Unzipper::OverwriteMode::DoNotOverwrite)); 420 | ASSERT_THAT(unzipper.error().message(), 421 | testing::HasSubstr("Failed navigating to first zip entry")); 422 | ASSERT_FALSE(unzipper.extractAll(Unzipper::OverwriteMode::Overwrite)); 423 | ASSERT_THAT(unzipper.error().message(), 424 | testing::HasSubstr("Failed navigating to first zip entry")); 425 | ASSERT_FALSE( 426 | unzipper.extract("test1.txt", Unzipper::OverwriteMode::Overwrite)); 427 | ASSERT_THAT(unzipper.error().message(), 428 | testing::HasSubstr("Unknown entry name")); 429 | unzipper.close(); 430 | } 431 | 432 | // Add file to the previously empty zip using Append mode 433 | { 434 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Append); 435 | EXPECT_TRUE(helper::zipAddFile( 436 | zipper, "test1.txt", "test1 file compression", "test1.txt")); 437 | zipper.close(); 438 | } 439 | 440 | // Verify entry was added successfully 441 | { 442 | zipper::Unzipper unzipper("ziptest.zip"); 443 | std::vector entries = unzipper.entries(); 444 | unzipper.close(); 445 | EXPECT_EQ(entries.size(), 1u); 446 | EXPECT_STREQ(entries[0].name.c_str(), "test1.txt"); 447 | } 448 | 449 | // Add file to the previously empty zip using Append mode 450 | { 451 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Append); 452 | EXPECT_TRUE(helper::zipAddFile( 453 | zipper, "test2.txt", "test2 file compression", "test2.txt")); 454 | zipper.close(); 455 | } 456 | 457 | // Verify entry was added successfully 458 | { 459 | zipper::Unzipper unzipper("ziptest.zip"); 460 | std::vector entries = unzipper.entries(); 461 | unzipper.close(); 462 | EXPECT_EQ(entries.size(), 2u); 463 | EXPECT_STREQ(entries[0].name.c_str(), "test1.txt"); 464 | EXPECT_STREQ(entries[1].name.c_str(), "test2.txt"); 465 | } 466 | 467 | ASSERT_TRUE(helper::removeFileOrDir("ziptest.zip")); 468 | } 469 | 470 | //============================================================================= 471 | // Tests for issue #1: Unicode file names 472 | //============================================================================= 473 | TEST(ZipTests, Issue1_01) 474 | { 475 | const std::string zipFilename(PWD "/issues/unicode2.zip"); 476 | const std::string file1 = "😊"; 477 | 478 | std::cout << "zipFilename: " << zipFilename << std::endl; 479 | 480 | Unzipper unzipper(zipFilename); 481 | EXPECT_EQ(unzipper.entries().size(), 1u); 482 | EXPECT_STREQ(unzipper.entries()[0].name.c_str(), file1.c_str()); 483 | 484 | ASSERT_TRUE(unzipper.extract(file1)); 485 | ASSERT_TRUE(helper::checkFileExists(file1, "Hello World!")); 486 | 487 | unzipper.close(); 488 | ASSERT_TRUE(helper::removeFileOrDir(file1)); 489 | } 490 | 491 | #if 0 492 | //============================================================================= 493 | // Tests for issue #1: Unicode file names 494 | //============================================================================= 495 | TEST(ZipTests, Issue1_02) 496 | { 497 | const std::string zipFilename(PWD "/issues/unicode.zip"); 498 | const std::string file1 = "😊"; 499 | 500 | Unzipper unzipper(zipFilename); 501 | EXPECT_EQ(unzipper.entries().size(), 1u); 502 | EXPECT_STREQ(unzipper.entries()[0].name.c_str(), file1.c_str()); 503 | 504 | ASSERT_TRUE(unzipper.extract(file1)); 505 | ASSERT_TRUE(helper::checkFileExists(file1, "Hello World!")); 506 | 507 | unzipper.close(); 508 | ASSERT_TRUE(helper::removeFileOrDir(file1)); 509 | } 510 | #endif 511 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Zipper](doc/logo.png) 2 | 3 | [Zipper](https://github.com/lecrapouille/zipper) is a C++14 wrapper around the minizip compression library. Its goal is to bring the power and simplicity of minizip to a more object-oriented and C++ user-friendly library. 4 | 5 | This project is a continuation of the original [project](https://github.com/sebastiandev/zipper). The original project was created out of the need for a compression library that would be reliable, simple, and flexible. By flexibility, we mean supporting various types of inputs and outputs, specifically the ability to compress into memory instead of being restricted to file compression only, and using data from memory instead of just files. 6 | 7 | This current fork repository was created because the original project was no longer maintained by its authors, and I, Lecrapouille, encountered issues due to missing administrative rights (needed for CI, branch management, API breaking changes, etc.). 8 | 9 | ## Zipper Features 10 | 11 | - [x] Create zip files in memory. 12 | - [x] Support for files, vectors, and generic streams as input for zipping. 13 | - [x] File mappings for replacement strategies (overwrite if exists or use alternative names from mapping). 14 | - [x] Password-protected zip (AES). 15 | - [x] Multi-platform support. 16 | - [x] Project compiles as both static and dynamic libraries. 17 | - [x] Protection flags against overwriting existing files during extraction. 18 | - [x] Protection against the [Zip Slip attack](https://security.snyk.io/research/zip-slip-vulnerability). 19 | - [x] API to detect [Zip Bomb attacks](https://www.bamsoftware.com/hacks/zipbomb/). Extraction is not recursive. 20 | - [x] Non-regression tests. 21 | 22 | **:warning: Security Notice** 23 | 24 | - Zipper currently uses an outdated (and potentially vulnerable) version of [minizip](https://github.com/zlib-ng/minizip-ng) from 2017 (SHA1 0bb5afeb0d3f23149b086ccda7e4fee7d48f4fdf) with some custom modifications. 25 | 26 | ## Getting Started 27 | 28 | There are two ways to compile the project: 29 | 30 | - Makefile: This is the official compilation method but only supports Linux and macOS 31 | - CMake: Recently added, it supports all operating systems and was primarily added for Windows support 32 | 33 | ### Compiling / Installing with Makefile (Linux, MacOs, not working for Windows) 34 | 35 | This is the official way to download the project and compile it: 36 | 37 | ```shell 38 | git clone https://github.com/lecrapouille/zipper.git --recursive 39 | cd zipper 40 | make download-external-libs 41 | make compile-external-libs 42 | make -j8 43 | ``` 44 | 45 | Explanations of compilation commands: 46 | 47 | - Git cloning requires the recursive option to install the Makefile helper and third-party libs (`zlib` and `minizip`) in the `external` folder. They are based on fixed SHA1. They are installed in the folder `external`. 48 | - Optionally `make download-external-libs` will git clone HEADs of third-party libs (`zlib` and `minizip`) in the `external` folder. It is optional since it was initially used instead of git submodule. 49 | - `make compile-external-libs` will compile third-party libs (`zlib` and `minizip`) but not install them on your operating system. They are compiled as static libraries and merged into this library inside the `build` folder. 50 | - `make` will compile this library against the third-party libs (`zlib` and `minizip`). A `build` folder is created with two demos inside. Note: `-j8` should be adapted to your number of CPU cores. 51 | 52 | See the [README](doc/demos/README.md) file for using the demos. To run demos, you can run them: 53 | 54 | ```shell 55 | cd build 56 | ./unzipper-demo -h 57 | ./zipper-demo -h 58 | ``` 59 | 60 | To install C++ header files, shared and static libraries on your operating system, type: 61 | 62 | ```shell 63 | sudo make install 64 | ``` 65 | 66 | You will see a message like: 67 | 68 | ```shell 69 | *** Installing: doc => /usr/share/Zipper/2.0.0/doc 70 | *** Installing: libs => /usr/lib 71 | *** Installing: pkg-config => /usr/lib/pkgconfig 72 | *** Installing: headers => /usr/include/Zipper-2.0.0 73 | *** Installing: Zipper => /usr/include/Zipper-2.0.0 74 | ``` 75 | 76 | For developers, you can run non regression tests. They depend on: 77 | 78 | - [googletest](https://github.com/google/googletest) framework 79 | - lcov for code coverage 80 | 81 | ```shell 82 | make tests -j8 83 | ``` 84 | 85 | ### Compiling / Installing with CMake (Linux, MacOs, Windows) 86 | 87 | As an alternative, you can also build the project using CMake: 88 | 89 | ```shell 90 | git clone https://github.com/lecrapouille/zipper.git --recursive 91 | cd zipper 92 | mkdir build 93 | cd build 94 | cmake .. -DZIPPER_SHARED_LIB=ON -DZIPPER_BUILD_DEMOS=ON -DZIPPER_BUILD_TESTS=ON 95 | # Either: 96 | cmake --build . --config Release 97 | # Or: make -j8 98 | ``` 99 | 100 | Optional options: 101 | 102 | - `-DZIPPER_SHARED_LIB=ON` allows creating a shared lib instead of static lib. 103 | - `-DZIPPER_BUILD_DEMOS=ON` allows compiling zipper and unzipper "hello world" demos. 104 | - `-DZIPPER_BUILD_TESTS=ON` allows compiling unit tests (if you are a developer). 105 | 106 | ### Linking Zipper to your project 107 | 108 | - In your project, add the needed headers in your C++ files: 109 | 110 | ```c++ 111 | #include 112 | #include 113 | ``` 114 | 115 | - To compile your project "as it" against Zipper, the simplest way is to use the `pkg-config` command: 116 | 117 | ```shell 118 | g++ -W -Wall --std=c++14 main.cpp -o prog `pkg-config zipper --cflags --libs` 119 | ``` 120 | 121 | - For Makefile: 122 | - set `LDFLAGS` to `pkg-config zipper --libs` 123 | - set `CPPFLAGS` to `pkg-config zipper --cflags` 124 | 125 | - For CMake: 126 | 127 | ```cmake 128 | find_package(zipper REQUIRED) 129 | target_link_libraries(your_application zipper::zipper) 130 | ``` 131 | 132 | You have an example [doc/demos/CMakeHelloWorld](here). 133 | 134 | ## API 135 | 136 | There are two classes available: `Zipper` and `Unzipper`. They behave in the same manner regarding constructors and storage parameters. 137 | 138 | ### Zipping API 139 | 140 | #### Header 141 | 142 | ```c++ 143 | #include 144 | using namespace zipper; 145 | ``` 146 | 147 | #### Constructor 148 | 149 | - Constructor without password and replace `ziptest.zip` if already present. The new zip archive is empty. The flag `Zipper::OpenFlags::Overwrite` is optional. 150 | 151 | ```c++ 152 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Overwrite); 153 | ``` 154 | 155 | - Constructor without password and preserve `ziptest.zip` if already present. The flag `Zipper::OpenFlags::Append` is mandatory! 156 | 157 | ```c++ 158 | Zipper zipper("ziptest.zip", Zipper::OpenFlags::Append); 159 | ``` 160 | 161 | - Constructor with password (using AES algorithm) and replace `ziptest.zip` if already present. The new zip archive is empty. The flag `Zipper::OpenFlags::Overwrite` is optional. 162 | 163 | ```c++ 164 | Zipper zipper("ziptest.zip", "my_password", Zipper::OpenFlags::Overwrite); 165 | ``` 166 | 167 | - Constructor with a password and preserve `ziptest.zip` if already present. The flag `Zipper::OpenFlags::Append` is mandatory! 168 | 169 | ```c++ 170 | Zipper zipper("ziptest.zip", "my_password", Zipper::OpenFlags::Append); 171 | ``` 172 | 173 | - Constructor for in-memory zip compression (storage inside std::iostream): 174 | 175 | ```c++ 176 | std::stringstream zipStream; 177 | Zipper zipper(zipStream); 178 | Zipper zipper(zipStream, Zipper::OpenFlags::Overwrite); 179 | Zipper zipper(zipStream, Zipper::OpenFlags::Append); 180 | Zipper zipper(zipStream, "my_password"); 181 | Zipper zipper(zipStream, "my_password", Zipper::OpenFlags::Overwrite); 182 | Zipper zipper(zipStream, "my_password", Zipper::OpenFlags::Append); 183 | ``` 184 | 185 | - Constructor for in-memory zip compression (storage inside std::vector): 186 | 187 | ```c++ 188 | std::vector zipVector; 189 | Zipper zipper(zipVector); 190 | Zipper zipper(zipVector, Zipper::OpenFlags::Overwrite); 191 | Zipper zipper(zipVector, Zipper::OpenFlags::Append); 192 | Zipper zipper(zipVector, "my_password"); 193 | Zipper zipper(zipVector, "my_password", Zipper::OpenFlags::Overwrite); 194 | Zipper zipper(zipVector, "my_password", Zipper::OpenFlags::Append); 195 | ``` 196 | 197 | - Note: all constructors will throw a `std::runtime_error` exception in case of failure. 198 | 199 | ```c++ 200 | try 201 | { 202 | Zipper zipper("ziptest.zip", ...); 203 | ... 204 | } 205 | catch (std::runtime_error const& e) 206 | { 207 | std::cerr << e.what() << std::endl; 208 | } 209 | ``` 210 | 211 | - If this is not a desired behavior, you can choose the alternative dummy constructor followed by the `open` method which takes the same arguments as constructors. This method does not throw but will return `false` in case of error, you can get the reason by calling `error()`. 212 | 213 | ```c++ 214 | // Dummy constructor 215 | Zipper zipper; 216 | 217 | // Same arguments than seen previously with constructors. 218 | if (!zipper.open(...)) 219 | { 220 | std::cerr << zipper.error() << std::endl; 221 | } 222 | ``` 223 | 224 | #### Closing / Reopening 225 | 226 | Do not forget to call `close()` explicitly (it's called implicitly from the destructor) otherwise 227 | the zip will not be well-formed and `Unzipper` (or any unzipper application) will fail to open it, for example. 228 | 229 | ```c++ 230 | Zipper zipper("ziptest.zip", ...); 231 | ... 232 | zipper.close(); // Now Unzipper unzipper("ziptest.zip") can work 233 | ``` 234 | 235 | After `close()` you can reopen the zip archive with `open()` without arguments. You can pass the same arguments than seen previously with constructors to open with new password or flags. Note: that any open method will call implicitly the close() method. 236 | 237 | ```c++ 238 | Zipper zipper("ziptest.zip", ...); 239 | ... 240 | zipper.close(); 241 | ... 242 | 243 | zipper.open(); 244 | ... 245 | zipper.close(); 246 | ``` 247 | 248 | #### Appending files or folders inside the archive 249 | 250 | The `add()` method allows appending files or folders. The `Zipper::ZipFlags::Better` is set implicitly. Other options are (as the last argument): 251 | 252 | - Store only: `Zipper::ZipFlags::Store`. 253 | - Compress faster, less compressed: `Zipper::ZipFlags::Faster`. 254 | - Compress intermediate time/compression: `Zipper::ZipFlags::Medium`. 255 | - Compress better: `Zipper::ZipFlags::Better`. 256 | - To preserve directory hierarchy add `| Zipper::ZipFlags::SaveHierarchy` else files are only stored. 257 | 258 | In case of success, the `add()` will return `true`; otherwise it will return `false` and `error()` should be used for getting the std::error_code. 259 | 260 | - Adding an entire folder to a zip: 261 | 262 | ```c++ 263 | Zipper zipper("ziptest.zip"); 264 | zipper.add("myFolder/"); 265 | zipper.close(); 266 | ``` 267 | 268 | - Adding a file by name: 269 | 270 | ```c++ 271 | Zipper zipper("ziptest.zip"); 272 | zipper.add("myFolder/somefile.txt"); 273 | zipper.close(); 274 | ``` 275 | 276 | - You can change their name in the archive: 277 | 278 | ```c++ 279 | Zipper zipper("ziptest.zip"); 280 | zipper.add("somefile.txt", "new_name_in_archive"); 281 | zipper.close(); 282 | ``` 283 | 284 | - Create a zip file with 2 files referred by their `std::ifstream` and change their name in the archive: 285 | 286 | ```c++ 287 | std::ifstream input1("first_file"); 288 | std::ifstream input2("second_file"); 289 | 290 | Zipper zipper("ziptest.zip"); 291 | zipper.add(input1, "Test1"); 292 | zipper.add(input2, "Test2"); 293 | zipper.close(); 294 | ``` 295 | 296 | Note that: 297 | 298 | - the `zipper::close()` updates `std::ifstream` and makes the in-memory zip well formed. 299 | - do not use std::ifstream before closed() was called. 300 | - be sure the std::ifstream is not deleted before closed() was called. 301 | 302 | - Add a file with a specific timestamp: 303 | 304 | ```c++ 305 | std::ifstream input("somefile.txt"); 306 | std::tm timestamp; 307 | timestamp.tm_year = 2024; 308 | timestamp.tm_mon = 0; 309 | timestamp.tm_mday = 1; 310 | timestamp.tm_hour = 12; 311 | timestamp.tm_min = 1; 312 | timestamp.tm_sec = 2; 313 | 314 | Zipper zipper("ziptest.zip"); 315 | zipper.add(input, timestamp, "somefile.txt"); 316 | zipper.close(); 317 | ``` 318 | 319 | - Zipper has security against [Zip Slip vulnerability](https://security.snyk.io/research/zip-slip-vulnerability). 320 | 321 | ```c++ 322 | zipper.add(input1, "../Test1"); 323 | ``` 324 | 325 | Will always return `false` because `Test1` would be extracted outside the destination folder. This prevents malicious attacks from replacing your system files: 326 | 327 | ```c++ 328 | zipper.add(malicious_passwd, "../../../../../../../../../../../../../../../etc/passwd"); 329 | ``` 330 | 331 | Because in Unix, trying to go outside the root folder `/` will stay in the root folder. Example: 332 | 333 | ```bash 334 | cd / 335 | pwd 336 | cd ../../../../../../../../../../../../../../.. 337 | pwd 338 | ``` 339 | 340 | - The Zipper lib forces canonical paths in the archive. The following code works (will return `true`): 341 | 342 | ```c++ 343 | zipper.add(input1, "foo/../Test1"); 344 | ``` 345 | 346 | because `foo/../Test1` is replaced by `Test1` (even if the folder `foo` is not present in the 347 | zip archive). 348 | 349 | #### In-memory: Vector and stream 350 | 351 | - Do not forget that the close() finalized the in-memory zip file: you cannot use Unzipper on the same memory until the Zipper::close() has been closed (meaning: even if you have called Zipper::add, the zip is not well formed). 352 | - Creating a zip file using the awesome streams from the [boost](https://www.boost.org/) library that lets us use a vector as a stream: 353 | 354 | ```c++ 355 | #include 356 | ... 357 | 358 | boost::interprocess::basic_vectorstream> input_data(some_vector); 359 | 360 | Zipper zipper("ziptest.zip"); 361 | zipper.add(input_data, "Test1"); 362 | zipper.close(); 363 | ``` 364 | 365 | - Creating a zip in a vector with files: 366 | 367 | ```c++ 368 | #include 369 | ... 370 | 371 | boost::interprocess::basic_vectorstream> zip_in_memory; 372 | std::ifstream input1("some file"); 373 | 374 | Zipper zipper(zip_in_memory); // You can pass password 375 | zipper.add(input1, "Test1"); 376 | zipper.close(); 377 | 378 | Unzipper unzipper(zip_in_memory); 379 | unzipper.extract(... 380 | ``` 381 | 382 | Or: 383 | 384 | ```c++ 385 | #include 386 | 387 | std::vector zip_vector; 388 | std::ifstream input1("some file"); 389 | 390 | Zipper zipper(zip_vector); // You can pass password 391 | zipper.add(input1, "Test1"); 392 | zipper.close(); 393 | ```- Creating a zip in-memory stream with files: 394 | 395 | ```c++ 396 | // Example of using stringstream 397 | std::stringstream zipStream; 398 | std::stringstream inputStream("content to zip"); 399 | 400 | Zipper zipper(zipStream); // You can pass password 401 | zipper.add(inputStream, "Test1"); 402 | zipper.close(); 403 | 404 | // Example of extracting 405 | zipper::Unzipper unzipper(zipData); // or unzipper(zipStream) for stringstream 406 | unzipper.extract(... 407 | ``` 408 | 409 | ### Unzipping API 410 | 411 | #### Header 412 | 413 | ```c++ 414 | #include 415 | using namespace zipper; 416 | ``` 417 | 418 | #### Constructor 419 | 420 | - Constructor without password and opening `ziptest.zip` (should already be present). 421 | 422 | ```c++ 423 | Unzipper unzipper("ziptest.zip"); 424 | ... 425 | unzipper.close(); 426 | ``` 427 | 428 | - Constructor with a password and opening `ziptest.zip` (should already be present). 429 | 430 | ```c++ 431 | Unzipper unzipper("ziptest.zip", "my_password"); 432 | ... 433 | unzipper.close(); 434 | ``` 435 | 436 | - Constructor for in-memory zip extraction (from std::iostream): 437 | 438 | ```c++ 439 | std::stringstream zipStream; 440 | // Without password 441 | Unzipper unzipper(zipStream); 442 | // Or with password: 443 | Unzipper unzipper(zipStream, "my_password"); 444 | ``` 445 | 446 | - Constructor for in-memory zip extraction (from std::vector): 447 | 448 | ```c++ 449 | // Without password 450 | std::vector zipVector; 451 | Unzipper unzipper(zipVector); 452 | // Or with password: 453 | Unzipper unzipper(zipVector, "my_password"); 454 | ``` 455 | 456 | - Note: all constructors will throw a `std::runtime_error` exception in case of failure. 457 | 458 | ```c++ 459 | try 460 | { 461 | Unzipper unzipper("ziptest.zip", ...); 462 | ... 463 | } 464 | catch (std::runtime_error const& e) 465 | { 466 | std::cerr << e.what() << std::endl; 467 | } 468 | ``` 469 | 470 | #### Getting the list of zip entries 471 | 472 | ```c++ 473 | Unzipper unzipper("zipfile.zip"); 474 | std::vector entries = unzipper.entries(); 475 | for (auto& it: unzipper.entries()) 476 | { 477 | std::cout << it.name << ": " 478 | << it.timestamp 479 | << std::endl; 480 | } 481 | unzipper.close(); 482 | ``` 483 | 484 | #### Getting the total uncompressed size 485 | 486 | ```c++ 487 | Unzipper unzipper("zipfile.zip"); 488 | size_t total_size = unzipper.sizeOnDisk(); 489 | if (total_size > MAX_ALLOWED_SIZE) { 490 | // Prevent zip bomb attack 491 | std::cerr << "Zip file too large!" << std::endl; 492 | return; 493 | } 494 | unzipper.close(); 495 | ``` 496 | 497 | #### Extracting all entries from the zip file 498 | 499 | Two methods are available: `extractAll()` for the whole archive and `extract()` for 500 | a single element in the archive. They return `true` in case of success 501 | or `false` in case of failure. In case of failure, use `unzipper.error();` 502 | to get the `std::error_code`. 503 | 504 | If you are scared of [Zip bomb attack](https://www.bamsoftware.com/hacks/zipbomb/) you can 505 | check the total uncompressed size of all entries in the zip archive by calling 506 | `unzipper.sizeOnDisk()` before `unzipper.extractAll(...)`. 507 | 508 | ```c++ 509 | // 1 gigabyte 510 | const size_t MAX_TOTAL_UNCOMPRESSED_BYTES (1 * 1024 * 1024 * 1024) 511 | 512 | Unzipper unzipper("zipfile.zip"); 513 | if (unzipper.sizeOnDisk() <= MAX_TOTAL_UNCOMPRESSED_BYTES) 514 | { 515 | unzipper.extractAll(Unzipper::OverwriteMode::Overwrite); 516 | } 517 | else 518 | { 519 | std::cerr << "Zip bomb attack prevented" << std::endl; 520 | } 521 | ``` 522 | 523 | - If you do not care about replacing existing files or folders: 524 | 525 | ```c++ 526 | Unzipper unzipper("zipfile.zip"); 527 | unzipper.extractAll(Unzipper::OverwriteMode::Overwrite); 528 | unzipper.close(); 529 | ``` 530 | 531 | - If you care about replacing existing files or folders. The method will fail (return `false`) 532 | if a file would be replaced and the method `error()` will give you information. 533 | 534 | ```c++ 535 | Unzipper unzipper("zipfile.zip"); 536 | unzipper.extractAll(); // equivalent to unzipper.extractAll(Unzipper::OverwriteMode::DoNotOverwrite); 537 | unzipper.close(); 538 | ``` 539 | 540 | - Extracting all entries from the zip file to the desired destination: 541 | 542 | ```c++ 543 | Unzipper unzipper("zipfile.zip"); 544 | unzipper.extractAll("/the/destination/path"); // Fail if a file exists (DoNotOverwrite is implicit) 545 | unzipper.extractAll("/the/destination/path", Unzipper::OverwriteMode::Overwrite); // Replace existing files 546 | unzipper.close(); 547 | ``` 548 | 549 | - Extracting all entries from the zip file using alternative names for existing files on disk: 550 | 551 | ```c++ 552 | std::map alternativeNames = { {"Test1", "alternative_name_test1"} }; 553 | Unzipper unzipper("zipfile.zip"); 554 | unzipper.extractAll(".", alternativeNames); 555 | unzipper.close(); 556 | ``` 557 | 558 | - Extracting a single entry from the zip file: 559 | 560 | ```c++ 561 | Unzipper unzipper("zipfile.zip"); 562 | unzipper.extract("entry name"); 563 | unzipper.close(); 564 | ``` 565 | 566 | Returns `true` in case of success or `false` in case of failure. 567 | In case of failure, use `unzipper.error();` to get the `std::error_code`. 568 | 569 | - Extracting a single entry from the zip file to destination: 570 | 571 | ```c++ 572 | Unzipper unzipper("zipfile.zip"); 573 | unzipper.extract("entry name", "/the/destination/path"); // Fail if a file exists (DoNotOverwrite is implicit) 574 | unzipper.extract("entry name", "/the/destination/path", Unzipper::OverwriteMode::Overwrite); // Replace existing file 575 | unzipper.close(); 576 | ``` 577 | 578 | Returns `true` in case of success or `false` in case of failure. 579 | In case of failure, use `unzipper.error();` to get the `std::error_code`. 580 | 581 | - Extracting a single entry from the zip file to memory (stream): 582 | 583 | ```c++ 584 | std::stringstream output; 585 | Unzipper unzipper("zipfile.zip"); 586 | unzipper.extract("entry name", output); 587 | unzipper.close(); 588 | ``` 589 | 590 | - Extracting a single entry from the zip file to memory (vector): 591 | 592 | ```c++ 593 | std::vector unzipped_entry; 594 | Unzipper unzipper("zipfile.zip"); 595 | unzipper.extract("entry name", unzipped_entry); 596 | unzipper.close(); 597 | ``` 598 | 599 | Returns `true` in case of success or `false` in case of failure. 600 | In case of failure, use `unzipper.error();` to get the `std::error_code`. 601 | 602 | - Extracting from a vector: 603 | 604 | ```c++ 605 | std::vector zip_vector; // Populated with Zipper zipper(zip_vect); 606 | 607 | Unzipper unzipper(zip_vector); 608 | unzipper.extract("Test1"); 609 | ``` 610 | 611 | - Zipper has security against [Zip Slip vulnerability](https://security.snyk.io/research/zip-slip-vulnerability): if an entry has a path outside the extraction folder (like `../foo.txt`) it 612 | will return `false` even if the replace option is set. 613 | 614 | #### Progress Callback 615 | 616 | You can monitor the extraction progress by setting a callback function. The callback provides information about: 617 | 618 | - Current status (OK, KO, InProgress) 619 | - Current file being extracted 620 | - Number of bytes read 621 | - Total bytes to extract 622 | - Number of files extracted 623 | - Total number of files 624 | 625 | Example: 626 | 627 | ```c++ 628 | Unzipper unzipper("zipfile.zip"); 629 | 630 | // Set the progress callback 631 | unzipper.setProgressCallback([](const Unzipper::Progress& progress) { 632 | switch (progress.status) { 633 | case Unzipper::Progress::Status::InProgress: 634 | std::cout << "Extracting: " << progress.current_file 635 | << " (" << progress.files_extracted << "/" 636 | << progress.total_files << " files) " 637 | << (progress.bytes_read * 100 / progress.total_bytes) 638 | << "%" << std::endl; 639 | break; 640 | case Unzipper::Progress::Status::OK: 641 | std::cout << "Extraction completed successfully" << std::endl; 642 | break; 643 | case Unzipper::Progress::Status::KO: 644 | std::cout << "Extraction failed" << std::endl; 645 | break; 646 | } 647 | }); 648 | 649 | // Start extraction 650 | unzipper.extractAll(); 651 | unzipper.close(); 652 | ``` 653 | 654 | Same idea for Zipper. 655 | 656 | ## FAQ 657 | 658 | - Q: I used a password when zipping with the Zipper lib, but now when I want to extract data with my operating system's zip tool, I get an error. 659 | - A: By default, Zipper uses the AES encryption algorithm, which is not the default encryption method for zip files. Your operating system's zip tools may not support AES. You can extract it using the 7-Zip tool: `7za e your.zip`. If you want to use the default zip encryption (at your own risk since the password can be cracked), you can remove the AES option in the following files: [Make](Make) and [external/compile-external-libs.sh](external/compile-external-libs.sh) and recompile the Zipper project. 660 | 661 | - Q: Why are there two classes: one for zipping and one for unzipping? Couldn't we have made a single class to handle both operations? 662 | - A: AFAIK this is a constraint imposed by the minizip API: No simultaneous operations are allowed - you cannot read from and write to a ZIP archive at the same time. 663 | 664 | - Q: How do I remove a file from the zip? 665 | - A: To modify an existing archive: 666 | - You generally need to create a temporary copy. 667 | - Copy the unchanged files. 668 | - Add the new files. 669 | - Replace the original. 670 | -------------------------------------------------------------------------------- /include/Zipper/Zipper.hpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // Copyright (c) 2022 Quentin Quadrat 3 | // https://github.com/Lecrapouille/zipper distributed under MIT License. 4 | // Based on https://github.com/sebastiandev/zipper/tree/v2.x.y distributed under 5 | // MIT License. Copyright (c) 2015 -- 2022 Sebastian 6 | //----------------------------------------------------------------------------- 7 | 8 | #ifndef ZIPPER_ZIPPER_HPP 9 | #define ZIPPER_ZIPPER_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | // ***************************************************************************** 22 | // Generated by CMake. Needed for exporting symbols for making the libzipper.dll 23 | // on Windows. 24 | // ***************************************************************************** 25 | #if defined(ZIPPER_EXPORT_DEFINED) 26 | # include "zipper_export.h" 27 | #else 28 | # define ZIPPER_EXPORT 29 | #endif 30 | 31 | namespace zipper 32 | { 33 | 34 | // ************************************************************************* 35 | //! \brief Zip archive compressor. 36 | // ************************************************************************* 37 | class ZIPPER_EXPORT Zipper 38 | { 39 | // Forward declaration 40 | struct Impl; 41 | 42 | public: 43 | 44 | // ------------------------------------------------------------------------- 45 | //! \brief Archive opening flags. 46 | // ------------------------------------------------------------------------- 47 | enum OpenFlags 48 | { 49 | //! \brief Overwrite existing zip file 50 | Overwrite, 51 | //! \brief Append to existing zip file 52 | Append 53 | }; 54 | 55 | // ------------------------------------------------------------------------- 56 | //! \brief Compression options for files. 57 | // ------------------------------------------------------------------------- 58 | enum ZipFlags 59 | { 60 | //! \brief Minizip option: -0 Store only (no compression). 61 | Store = 0x00, 62 | //! \brief Minizip option: -1 Compress faster (less compression). 63 | Faster = 0x01, 64 | //! \brief Minizip option: -5 Medium compression. 65 | Medium = 0x05, 66 | //! \brief Minizip option: -9 Better compression (slower). 67 | Better = 0x09, 68 | //! \brief Preserve directory hierarchy when adding files. 69 | SaveHierarchy = 0x40 70 | }; 71 | 72 | // ------------------------------------------------------------------------- 73 | //! \brief Structure holding progress information during compression 74 | // ------------------------------------------------------------------------- 75 | struct Progress 76 | { 77 | //! \brief Status of the compression operation 78 | enum class Status 79 | { 80 | OK, //!< Compression completed successfully 81 | KO, //!< Compression failed 82 | InProgress //!< Compression in progress 83 | }; 84 | 85 | Status status = Status::InProgress; //!< Current status 86 | std::string current_file; //!< Name of the current file being compressed 87 | uint64_t bytes_processed = 0; //!< Number of bytes processed so far 88 | uint64_t total_bytes = 0; //!< Total number of bytes to compress 89 | uint64_t files_compressed = 0; //!< Number of files compressed 90 | uint64_t total_files = 0; //!< Total number of files to compress 91 | }; 92 | 93 | //! \brief Callback type for reporting compression progress 94 | using ProgressCallback = std::function; 95 | 96 | // ------------------------------------------------------------------------- 97 | //! \brief Default constructor. You shall use open() method to initialize 98 | //! the zipper. 99 | // ------------------------------------------------------------------------- 100 | Zipper(); 101 | 102 | // ------------------------------------------------------------------------- 103 | //! \brief Regular zip compression (inside a disk zip archive file) with a 104 | //! password. 105 | //! 106 | //! \param[in] p_zip_name Path where to create the zip file. 107 | //! \param[in] p_password Optional password (empty for no password 108 | //! protection). 109 | //! \param[in] p_open_flags Overwrite (default) or append to existing 110 | //! zip file. 111 | //! \throw std::runtime_error if an error occurs during 112 | //! initialization. 113 | // ------------------------------------------------------------------------- 114 | Zipper(const std::string& p_zip_name, 115 | const std::string& p_password, 116 | Zipper::OpenFlags p_open_flags = Zipper::OpenFlags::Overwrite); 117 | 118 | // ------------------------------------------------------------------------- 119 | //! \brief Regular zip compression (inside a disk zip archive file) without 120 | //! password. 121 | //! 122 | //! \param[in] p_zipname Path where to create the zip file. 123 | //! \param[in] p_open_flags Overwrite (default) or append to existing zip 124 | //! file. \throw std::runtime_error if an error occurs during 125 | //! initialization. 126 | // ------------------------------------------------------------------------- 127 | Zipper(const std::string& p_zipname, 128 | Zipper::OpenFlags p_open_flags = Zipper::OpenFlags::Overwrite) 129 | : Zipper(p_zipname, std::string(), p_open_flags) 130 | { 131 | } 132 | 133 | // ------------------------------------------------------------------------- 134 | //! \brief In-memory zip compression (storage inside std::iostream). 135 | //! 136 | //! \param[in,out] p_buffer Stream in which to store zipped files. Must 137 | //! remain valid for the lifetime of the Zipper object. 138 | //! \param[in] p_open_flags Optional flags to control how the zip is opened. 139 | //! \throw std::runtime_error if an error occurs during 140 | //! initialization. 141 | // ------------------------------------------------------------------------- 142 | Zipper(std::iostream& p_buffer, Zipper::OpenFlags p_open_flags) 143 | : Zipper(p_buffer, std::string(), p_open_flags) 144 | { 145 | } 146 | 147 | // ------------------------------------------------------------------------- 148 | //! \brief In-memory zip compression (storage inside std::iostream). 149 | //! 150 | //! \param[in,out] p_buffer Stream in which to store zipped files. Must 151 | //! remain valid for the lifetime of the Zipper object. 152 | //! \param[in] p_password Optional password (empty for no password 153 | //! protection). 154 | //! \param[in] p_open_flags Optional flags to control how the zip is opened. 155 | //! \throw std::runtime_error if an error occurs during 156 | //! initialization. 157 | // ------------------------------------------------------------------------- 158 | Zipper(std::iostream& p_buffer, 159 | const std::string& p_password, 160 | Zipper::OpenFlags p_open_flags); 161 | 162 | // ------------------------------------------------------------------------- 163 | //! \brief In-memory zip compression (storage inside std::iostream). 164 | //! 165 | //! \param[in,out] p_buffer Stream in which to store zipped files. Must 166 | //! remain valid for the lifetime of the Zipper object. 167 | //! \param[in] p_password Optional password (empty for no password 168 | //! protection). 169 | //! \throw std::runtime_error if an error occurs during 170 | //! initialization. 171 | // ------------------------------------------------------------------------- 172 | Zipper(std::iostream& p_buffer, const std::string& p_password); 173 | 174 | // ------------------------------------------------------------------------- 175 | //! \brief In-memory zip compression (storage inside std::vector). 176 | //! 177 | //! \param[in,out] p_buffer Vector in which to store zipped files. Must 178 | //! remain valid for the lifetime of the Zipper object. 179 | //! \param[in] p_password Optional password (empty for no password 180 | //! protection). 181 | //! \throw std::runtime_error if an error occurs during 182 | //! initialization. 183 | // ------------------------------------------------------------------------- 184 | Zipper(std::vector& p_buffer, 185 | const std::string& p_password = std::string()); 186 | 187 | // ------------------------------------------------------------------------- 188 | //! \brief Destructor automatically calls close(). 189 | // ------------------------------------------------------------------------- 190 | ~Zipper(); 191 | 192 | // Disable copy and move semantics 193 | Zipper(const Zipper&) = delete; 194 | Zipper& operator=(const Zipper&) = delete; 195 | Zipper(Zipper&&) = delete; 196 | Zipper& operator=(Zipper&&) = delete; 197 | 198 | // ------------------------------------------------------------------------- 199 | //! \brief Compress data from source with a given timestamp in the archive 200 | //! with the given name. 201 | //! 202 | //! \param[in,out] p_source Data stream to compress. 203 | //! \param[in] p_timestamp Desired timestamp for the file. 204 | //! \param[in] p_name_in_zip Desired name for the file inside the archive. 205 | //! \param[in] p_flags Compression options (faster, better, etc.). 206 | //! \return true on success, false on failure. Sets internal error code on 207 | //! failure. 208 | // ------------------------------------------------------------------------- 209 | bool add(std::istream& p_source, 210 | const std::tm& p_timestamp, 211 | const std::string& p_name_in_zip, 212 | Zipper::ZipFlags p_flags = Zipper::ZipFlags::Better); 213 | 214 | // ------------------------------------------------------------------------- 215 | //! \brief Compress data from source in the archive with the given name. 216 | //! Uses current time as timestamp. 217 | //! 218 | //! \param[in,out] p_source Data stream to compress. 219 | //! \param[in] p_name_in_zip Desired name for the file inside the archive. 220 | //! \param[in] p_flags Compression options (faster, better, etc.). 221 | //! \return true on success, false on failure. Sets internal error code on 222 | //! failure. 223 | // ------------------------------------------------------------------------- 224 | bool add(std::istream& p_source, 225 | const std::string& p_name_in_zip, 226 | Zipper::ZipFlags p_flags = Zipper::ZipFlags::Better); 227 | 228 | // ------------------------------------------------------------------------- 229 | //! \brief Compress data from source in the archive with an empty name. 230 | //! Uses current time as timestamp. 231 | //! 232 | //! \param[in,out] p_source Data stream to compress. 233 | //! \param[in] p_flags Compression options (faster, better, etc.). 234 | //! \return true on success, false on failure. Sets internal error code on 235 | //! failure. 236 | // ------------------------------------------------------------------------- 237 | bool add(std::istream& p_source, 238 | Zipper::ZipFlags p_flags = Zipper::ZipFlags::Better) 239 | { 240 | return add(p_source, std::string(), p_flags); 241 | } 242 | 243 | // ------------------------------------------------------------------------- 244 | //! \brief Compress a folder or a file in the archive. 245 | //! 246 | //! \param[in] p_file_or_folder_path Path to the file or folder to compress. 247 | //! \param[in] p_flags Compression options (faster, better, etc.). If 248 | //! Zipper::ZipFlags::SaveHierarchy is included, the directory structure 249 | //! relative to the input path is preserved. 250 | //! \return true on success (all files added), false on failure (at least 251 | //! one file failed). Sets internal error code on failure. 252 | // ------------------------------------------------------------------------- 253 | bool add(const std::string& p_file_or_folder_path, 254 | Zipper::ZipFlags p_flags = Zipper::ZipFlags::Better); 255 | 256 | // ------------------------------------------------------------------------- 257 | //! \brief Compress a folder or a file in the archive. 258 | //! 259 | //! \param[in] p_file_or_folder_path Path to the file or folder to compress. 260 | //! \param[in] p_flags Compression options (faster, better, etc.). If 261 | //! Zipper::ZipFlags::SaveHierarchy is included, the directory structure 262 | //! relative to the input path is preserved. 263 | //! \return true on success (all files added), false on failure (at least 264 | //! one file failed). Sets internal error code on failure. 265 | // ------------------------------------------------------------------------- 266 | bool add(const char* p_file_or_folder_path, 267 | Zipper::ZipFlags p_flags = Zipper::ZipFlags::Better) 268 | { 269 | return add(std::string(p_file_or_folder_path), p_flags); 270 | } 271 | 272 | // ------------------------------------------------------------------------- 273 | //! \brief Closes the zip archive. 274 | //! 275 | //! Depending on the constructor used, this method will close the access to 276 | //! the zip file, flush the stream, or update the memory vector. 277 | //! \note This method is automatically called by the destructor. It's safe 278 | //! to call multiple times. 279 | // ------------------------------------------------------------------------- 280 | void close(); 281 | 282 | // ------------------------------------------------------------------------- 283 | //! \brief Opens or reopens the zip archive. 284 | //! 285 | //! If the archive was previously closed, this re-initializes it for 286 | //! appending or overwriting based on the mode it was originally created 287 | //! with (file, stream, or vector) and the provided flags. 288 | //! \param[in] p_flags Overwrite or append (default) to existing zip file. 289 | //! Only relevant for file-based zippers. 290 | //! \return true on success, false on failure. Sets internal error code on 291 | //! failure. 292 | //! \note This method is not called by the constructor. If called on an 293 | //! already open zipper, it first closes it. 294 | // ------------------------------------------------------------------------- 295 | bool open(const std::string& p_zip_name, 296 | const std::string& p_password, 297 | Zipper::OpenFlags p_open_flags = Zipper::OpenFlags::Overwrite); 298 | 299 | // ------------------------------------------------------------------------- 300 | //! \brief Open the zip archive from a file. 301 | //! 302 | //! \param[in] p_zipname Path to the zip file. 303 | //! \param[in] p_open_flags Overwrite (default) or append to existing zip 304 | //! file. 305 | //! \return true on success, false on failure. Sets internal error code on 306 | //! failure. 307 | // ------------------------------------------------------------------------- 308 | bool open(const std::string& p_zipname, 309 | Zipper::OpenFlags p_open_flags = Zipper::OpenFlags::Overwrite); 310 | 311 | // ------------------------------------------------------------------------- 312 | //! \brief Open the zip archive from a stream. 313 | //! 314 | //! \param[in,out] p_buffer Stream in which to store zipped files. Must 315 | //! remain valid for the lifetime of the Zipper object. 316 | //! \param[in] p_password Optional password (empty for no password 317 | //! protection). 318 | //! \param[in] p_open_flags Overwrite (default) or append to existing zip 319 | //! file. 320 | //! \return true on success, false on failure. Sets internal error code on 321 | //! failure. 322 | // ------------------------------------------------------------------------- 323 | bool open(std::iostream& p_buffer, 324 | const std::string& p_password, 325 | Zipper::OpenFlags p_open_flags); 326 | 327 | // ------------------------------------------------------------------------- 328 | //! \brief Open the zip archive from a stream. 329 | //! 330 | //! \param[in,out] p_buffer Stream in which to store zipped files. Must 331 | //! remain valid for the lifetime of the Zipper object. 332 | //! \param[in] p_password Optional password (empty for no password 333 | //! protection). 334 | //! \return true on success, false on failure. Sets internal error code on 335 | //! failure. 336 | // ------------------------------------------------------------------------- 337 | bool open(std::iostream& p_buffer, 338 | const std::string& p_password = std::string()); 339 | 340 | // ------------------------------------------------------------------------- 341 | //! \brief Open the zip archive from a vector. 342 | //! 343 | //! \param[in,out] p_buffer Vector in which to store zipped files. Must 344 | //! remain valid for the lifetime of the Zipper object. 345 | //! \param[in] p_password Optional password (empty for no password 346 | //! protection). 347 | //! \return true on success, false on failure. Sets internal error code on 348 | //! failure. 349 | // ------------------------------------------------------------------------- 350 | bool open(std::vector& p_buffer, 351 | const std::string& p_password = std::string()); 352 | 353 | // ------------------------------------------------------------------------- 354 | //! \brief Close and reopen the zipper. 355 | //! \return true on success, false on failure. Sets internal error code on 356 | //! failure. 357 | // ------------------------------------------------------------------------- 358 | bool reopen(); 359 | 360 | // ------------------------------------------------------------------------- 361 | //! \brief Check if the zipper is currently open and ready for adding files. 362 | //! \return True if the zipper is open, false otherwise. 363 | // ------------------------------------------------------------------------- 364 | inline bool isOpened() const 365 | { 366 | return m_open; 367 | } 368 | 369 | // ------------------------------------------------------------------------- 370 | //! \brief Get the error information when a method returned false. 371 | //! \return Reference to the error code. The error code is cleared on the 372 | //! next successful operation or successful open(). 373 | // ------------------------------------------------------------------------- 374 | inline std::error_code const& error() const 375 | { 376 | return m_error_code; 377 | } 378 | 379 | // ------------------------------------------------------------------------- 380 | //! \brief Set the progress callback function. 381 | //! \param[in] callback The function to call with progress updates. 382 | //! \return true if the callback was set successfully. 383 | // ------------------------------------------------------------------------- 384 | bool setProgressCallback(ProgressCallback callback); 385 | 386 | private: 387 | 388 | // ------------------------------------------------------------------------- 389 | //! \brief Check if the zipper is valid. 390 | //! \return true if the zipper is valid, false otherwise. 391 | // ------------------------------------------------------------------------- 392 | bool checkValid(); 393 | 394 | private: 395 | 396 | //! \brief Stream to store zipped files, if using stream constructor. 397 | //! Null otherwise. 398 | std::iostream* m_output_stream = nullptr; 399 | //! \brief Vector to store zipped files, if using vector constructor. 400 | //! Null otherwise. 401 | std::vector* m_output_vector = nullptr; 402 | //! \brief Name of the zip file, if using file constructor. 403 | std::string m_zip_name; 404 | //! \brief Password for the zip file. 405 | std::string m_password; 406 | //! \brief Overwrite or append (default) to existing zip file. 407 | OpenFlags m_open_flags = OpenFlags::Overwrite; 408 | //! \brief Whether the zip archive is currently open. 409 | bool m_open = false; 410 | //! \brief Stores the last error. 411 | std::error_code m_error_code; 412 | //! \brief PIMPL implementation detail. 413 | std::unique_ptr m_impl; 414 | }; 415 | 416 | // ------------------------------------------------------------------------- 417 | //! \name Bitwise operators for Zipper::ZipFlags 418 | // ------------------------------------------------------------------------- 419 | 420 | //! \brief Bitwise OR operator for Zipper::ZipFlags. 421 | //! \param[in] lhs Left-hand side operand. 422 | //! \param[in] rhs Right-hand side operand. 423 | //! \return Result of the bitwise OR operation. 424 | inline constexpr Zipper::ZipFlags operator|(Zipper::ZipFlags lhs, 425 | Zipper::ZipFlags rhs) 426 | { 427 | using T = std::underlying_type_t; 428 | return static_cast(static_cast(lhs) | 429 | static_cast(rhs)); 430 | } 431 | 432 | //! \brief Bitwise AND operator for Zipper::ZipFlags. 433 | //! \param[in] lhs Left-hand side operand. 434 | //! \param[in] rhs Right-hand side operand. 435 | //! \return Result of the bitwise AND operation. 436 | inline constexpr Zipper::ZipFlags operator&(Zipper::ZipFlags lhs, 437 | Zipper::ZipFlags rhs) 438 | { 439 | using T = std::underlying_type_t; 440 | return static_cast(static_cast(lhs) & 441 | static_cast(rhs)); 442 | } 443 | 444 | //! \brief Bitwise XOR operator for Zipper::ZipFlags. 445 | //! \param[in] lhs Left-hand side operand. 446 | //! \param[in] rhs Right-hand side operand. 447 | //! \return Result of the bitwise XOR operation. 448 | inline constexpr Zipper::ZipFlags operator^(Zipper::ZipFlags lhs, 449 | Zipper::ZipFlags rhs) 450 | { 451 | using T = std::underlying_type_t; 452 | return static_cast(static_cast(lhs) ^ 453 | static_cast(rhs)); 454 | } 455 | 456 | //! \brief Bitwise NOT operator for Zipper::ZipFlags. 457 | //! \param[in] flag Operand. 458 | //! \return Result of the bitwise NOT operation. 459 | inline constexpr Zipper::ZipFlags operator~(Zipper::ZipFlags flag) 460 | { 461 | using T = std::underlying_type_t; 462 | return static_cast(~static_cast(flag)); 463 | } 464 | 465 | //! \brief Bitwise OR assignment operator for Zipper::ZipFlags. 466 | //! \param[in,out] lhs Left-hand side operand. 467 | //! \param[in] rhs Right-hand side operand. 468 | //! \return Reference to the modified left-hand side operand. 469 | inline Zipper::ZipFlags& operator|=(Zipper::ZipFlags& lhs, Zipper::ZipFlags rhs) 470 | { 471 | lhs = lhs | rhs; 472 | return lhs; 473 | } 474 | 475 | //! \brief Bitwise AND assignment operator for Zipper::ZipFlags. 476 | //! \param[in,out] lhs Left-hand side operand. 477 | //! \param[in] rhs Right-hand side operand. 478 | //! \return Reference to the modified left-hand side operand. 479 | inline Zipper::ZipFlags& operator&=(Zipper::ZipFlags& lhs, Zipper::ZipFlags rhs) 480 | { 481 | lhs = lhs & rhs; 482 | return lhs; 483 | } 484 | 485 | //! \brief Bitwise XOR assignment operator for Zipper::ZipFlags. 486 | //! \param[in,out] lhs Left-hand side operand. 487 | //! \param[in] rhs Right-hand side operand. 488 | //! \return Reference to the modified left-hand side operand. 489 | inline Zipper::ZipFlags& operator^=(Zipper::ZipFlags& lhs, Zipper::ZipFlags rhs) 490 | { 491 | lhs = lhs ^ rhs; 492 | return lhs; 493 | } 494 | 495 | } // namespace zipper 496 | 497 | #endif // ZIPPER_ZIPPER_HPP 498 | --------------------------------------------------------------------------------