diff --git a/CMakeLists.txt b/CMakeLists.txt index 464c31b..b14bfec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,13 +120,16 @@ if(PROJECT_BUILD) @ONLY ) - if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.iss.in") + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}/${PROJECT_NAME}.iss.in") configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.iss.in + ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}/${PROJECT_NAME}.iss.in ${PROJECT_DIST_DIR}/../${PROJECT_NAME}.iss @ONLY ) endif() + + find_package(ICU REQUIRED COMPONENTS uc i18n io) + link_libraries(ICU::uc ICU::i18n ICU::io) else() message(STATUS "-=[CMake Settings]=-") message(STATUS " C standard: ${CMAKE_C_STANDARD}") @@ -180,6 +183,8 @@ endif() -DPROJECT_IS_MINGW=${PROJECT_IS_MINGW} -DPROJECT_IS_MINGW_UNIX=${PROJECT_IS_MINGW_UNIX} -DPROJECT_MAJOR_VERSION=${PROJECT_MAJOR_VERSION} + -DPROJECT_MACOS_BUNDLE_ID=${PROJECT_MACOS_BUNDLE_ID} + -DPROJECT_MACOS_ICNS_NAME=${PROJECT_MACOS_ICNS_NAME} -DPROJECT_MINOR_VERSION=${PROJECT_MINOR_VERSION} -DPROJECT_NAME=${PROJECT_NAME} -DPROJECT_RELEASE_ITER=${PROJECT_RELEASE_ITER} diff --git a/cmake/functions.cmake b/cmake/functions.cmake index 2bcedf8..e365a61 100644 --- a/cmake/functions.cmake +++ b/cmake/functions.cmake @@ -1,3 +1,7 @@ +if (PROJECT_MACOS_ICNS_NAME) + set(PROJECT_MACOS_ICNS_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/assets/${PROJECT_MACOS_ICNS_NAME}") +endif() + function(set_common_target_options name) target_compile_definitions(${name} PUBLIC ${PROJECT_DEFINITIONS} @@ -12,6 +16,16 @@ function(set_common_target_options name) ${PROJECT_EXTERNAL_BUILD_ROOT}/lib ) + if (PROJECT_STATIC_LINK) + target_compile_definitions(${name} PRIVATE U_STATIC_IMPLEMENTATION) + endif() + + target_link_directories(${name} PRIVATE + ICU::uc + ICU::i18n + ICU::io + ) + target_include_directories(${name} AFTER PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}/${name}/include ${name}_INCLUDES @@ -31,11 +45,31 @@ function(add_project_executable2 name dependencies libraries headers sources is_ list(APPEND sources ${PROJECT_WINDOWS_VERSION_RC}) endif() - add_executable(${name} - ${headers} - ${sources} - ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}/${name}/main.cpp - ) + if (PROJECT_IS_DARWIN AND PROJECT_MACOS_ICNS_SOURCE AND "${name}" STREQUAL "${PROJECT_NAME}") + set_source_files_properties(${PROJECT_MACOS_ICNS_SOURCE} PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) + + add_executable(${name} + MACOSX_BUNDLE + ${headers} + ${sources} + ${PROJECT_MACOS_ICNS_SOURCE} + ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}/${name}/main.cpp + ) + + set_target_properties(${name} PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_ICON_FILE "${PROJECT_MACOS_ICNS_NAME}" + RESOURCE "${PROJECT_MACOS_ICNS_SOURCE}" + ) + else() + add_executable(${name} + ${headers} + ${sources} + ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}/${name}/main.cpp + ) + endif() foreach(dependency ${dependencies}) set_common_target_options(${dependency}) diff --git a/cmake/libraries.cmake b/cmake/libraries.cmake index ac25e56..bd77d0e 100644 --- a/cmake/libraries.cmake +++ b/cmake/libraries.cmake @@ -4,12 +4,16 @@ set(Boost_USE_STATIC_LIBS ${PROJECT_STATIC_LINK}) set(CURL_USE_STATIC_LIBS ${PROJECT_STATIC_LINK}) set(OPENSSL_USE_STATIC_LIBS ${PROJECT_STATIC_LINK}) set(SFML_STATIC_LIBRARIES ${PROJECT_STATIC_LINK}) -set(ZLIB_USE_STATIC_LIBS ${PROJECT_STATIC_LINK}) +if (PROJECT_IS_DARWIN) + set(ZLIB_USE_STATIC_LIBS OFF) +else() + set(ZLIB_USE_STATIC_LIBS ${PROJECT_STATIC_LINK}) +endif() set(wxWidgets_USE_STATIC ${PROJECT_STATIC_LINK}) +set(ICU_USE_STATIC_LIBS ${PROJECT_STATIC_LINK}) +include(cmake/libraries/icu.cmake) include(cmake/libraries/openssl.cmake) - - include(cmake/libraries/backward_cpp.cmake) include(cmake/libraries/cpp_httplib.cmake) include(cmake/libraries/curl.cmake) diff --git a/cmake/libraries/cpp_httplib.cmake b/cmake/libraries/cpp_httplib.cmake index 019075b..57c0ece 100644 --- a/cmake/libraries/cpp_httplib.cmake +++ b/cmake/libraries/cpp_httplib.cmake @@ -15,6 +15,7 @@ if(PROJECT_ENABLE_CPP_HTTPLIB) CMAKE_ARGS ${PROJECT_EXTERNAL_CMAKE_FLAGS} -DBUILD_SHARED_LIBS=${PROJECT_BUILD_SHARED_LIBS} -DBUILD_STATIC_LIBS=ON + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} -DHTTPLIB_REQUIRE_BROTLI=OFF -DHTTPLIB_REQUIRE_OPENSSL=ON -DHTTPLIB_REQUIRE_ZLIB=ON diff --git a/cmake/libraries/curl.cmake b/cmake/libraries/curl.cmake index ae0cee7..133f17a 100644 --- a/cmake/libraries/curl.cmake +++ b/cmake/libraries/curl.cmake @@ -27,8 +27,8 @@ if(PROJECT_ENABLE_CURL) -DBUILD_SHARED_LIBS=${PROJECT_BUILD_SHARED_LIBS} -DBUILD_STATIC_CURL=ON -DBUILD_STATIC_LIBS=ON - -DBUILD_STATIC_LIBS=ON -DBUILD_TESTING=OFF + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} -DCURL_BROTLI=OFF -DCURL_CA_BUNDLE=./cacert.pem -DCURL_CA_FALLBACK=ON @@ -37,11 +37,13 @@ if(PROJECT_ENABLE_CURL) -DCURL_USE_LIBSSH2=OFF -DCURL_USE_OPENSSL=${PROJECT_ENABLE_OPENSSL} -DCURL_ZLIB=ON + -DCURL_ZSTD=OFF -DENABLE_CURL_MANUAL=OFF -DENABLE_THREADED_RESOLVER=ON -DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR} -DOPENSSL_USE_STATIC_LIBS=${OPENSSL_USE_STATIC_LIBS} -DUSE_LIBIDN2=OFF + -DUSE_NGHTTP2=OFF -DZLIB_USE_STATIC_LIBS=${ZLIB_USE_STATIC_LIBS} ) diff --git a/cmake/libraries/icu.cmake b/cmake/libraries/icu.cmake new file mode 100644 index 0000000..f278b9d --- /dev/null +++ b/cmake/libraries/icu.cmake @@ -0,0 +1,24 @@ +if(PROJECT_IS_DARWIN AND NOT PROJECT_BUILD) + if(PROJECT_BUILD_SHARED_LIBS) + set(ICU_ENABLE_SHARED yes) + else() + set(ICU_ENABLE_SHARED no) + endif() + + ExternalProject_Add(icu_project + PREFIX external + URL ${PROJECT_3RD_PARTY_DIR}/mingw64/icu-release-${ICU_VERSION}.tar.gz + URL_HASH SHA256=${ICU_HASH} + BUILD_IN_SOURCE 1 + LIST_SEPARATOR | + PATCH_COMMAND chmod +x ${PROJECT_3RD_PARTY_DIR}/icu_configure.sh + CONFIGURE_COMMAND cd icu4c/source && ${PROJECT_3RD_PARTY_DIR}/icu_configure.sh + ${PROJECT_MARCH} + ${PROJECT_EXTERNAL_BUILD_ROOT} + ${ICU_ENABLE_SHARED} + BUILD_COMMAND cd icu4c/source && make -j$ENV{CMAKE_BUILD_PARALLEL_LEVEL} + INSTALL_COMMAND cd icu4c/source && make install + ) + + list(APPEND PROJECT_DEPENDENCIES icu_project) +endif() diff --git a/cmake/libraries/json.cmake b/cmake/libraries/json.cmake index 632e6ba..1a08005 100644 --- a/cmake/libraries/json.cmake +++ b/cmake/libraries/json.cmake @@ -18,6 +18,7 @@ if(PROJECT_ENABLE_JSON) CMAKE_ARGS ${PROJECT_EXTERNAL_CMAKE_FLAGS} -DBUILD_SHARED_LIBS=${PROJECT_BUILD_SHARED_LIBS} -DBUILD_STATIC_LIBS=ON + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} -DJSON_BuildTests=OFF -DJSON_Install=ON -DJSON_MultipleHeaders=OFF diff --git a/cmake/libraries/rocksdb.cmake b/cmake/libraries/rocksdb.cmake index 123748a..acf9d42 100644 --- a/cmake/libraries/rocksdb.cmake +++ b/cmake/libraries/rocksdb.cmake @@ -14,12 +14,14 @@ if(PROJECT_ENABLE_ROCKSDB) CMAKE_ARGS ${PROJECT_EXTERNAL_CMAKE_FLAGS} -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} -DFAIL_ON_WARNINGS=OFF -DPORTABLE=1 -DROCKSDB_BUILD_SHARED=OFF -DROCKSDB_INSTALL_ON_WINDOWS=ON -DWITH_BENCHMARK=OFF -DWITH_BENCHMARK_TOOLS=OFF + -DWITH_BZ2=OFF -DWITH_CORE_TOOLS=OFF -DWITH_EXAMPLES=OFF -DWITH_GFLAGS=OFF diff --git a/cmake/libraries/spdlog.cmake b/cmake/libraries/spdlog.cmake index 21972d5..f5dd23a 100644 --- a/cmake/libraries/spdlog.cmake +++ b/cmake/libraries/spdlog.cmake @@ -15,6 +15,7 @@ if(PROJECT_ENABLE_SPDLOG) LIST_SEPARATOR | CMAKE_ARGS ${PROJECT_EXTERNAL_CMAKE_FLAGS} -DBUILD_SHARED_LIBS=${PROJECT_BUILD_SHARED_LIBS} + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} -DSPDLOG_BUILD_EXAMPLE=OFF -DSPDLOG_FMT_EXTERNAL=OFF -DSPDLOG_FMT_EXTERNAL_HO=OFF diff --git a/cmake/libraries/testing.cmake b/cmake/libraries/testing.cmake index 5161d42..a0012c6 100644 --- a/cmake/libraries/testing.cmake +++ b/cmake/libraries/testing.cmake @@ -10,6 +10,7 @@ if (PROJECT_ENABLE_TESTING) CMAKE_ARGS ${PROJECT_EXTERNAL_CMAKE_FLAGS} -DBUILD_SHARED_LIBS=${PROJECT_BUILD_SHARED_LIBS} -DBUILD_STATIC_LIBS=ON + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} ) list(APPEND PROJECT_DEPENDENCIES gtest_project) diff --git a/cmake/os.cmake b/cmake/os.cmake index 7a72524..b8f2a10 100644 --- a/cmake/os.cmake +++ b/cmake/os.cmake @@ -9,3 +9,15 @@ endif() if(PROJECT_REQUIRE_ALPINE AND NOT PROJECT_IS_ALPINE AND PROJECT_IS_MINGW AND PROJECT_IS_MINGW_UNIX) message(FATAL_ERROR "Project requires Alpine Linux to build") endif() + +if (PROJECT_IS_DARWIN) + if (PROJECT_IS_ARM64) + set(CMAKE_OSX_ARCHITECTURES "arm64") + else() + set(CMAKE_OSX_ARCHITECTURES "x86_64") + endif() +endif() + +if (PROJECT_IS_DARWIN AND NOT PROJECT_MACOS_BUNDLE_ID) + message(FATAL_ERROR "'PROJECT_MACOS_BUNDLE_ID' is not set in 'config.sh'") +endif() diff --git a/docker/x86_64/flutter b/docker/x86_64/flutter index 20e1422..e086270 100644 --- a/docker/x86_64/flutter +++ b/docker/x86_64/flutter @@ -11,7 +11,6 @@ RUN apt-get install -y \ gdb \ git \ lib32stdc++6 \ - libgconf-2-4 \ libglu1-mesa \ libstdc++6 \ python3 \ diff --git a/docker/x86_64/mingw64 b/docker/x86_64/mingw64 index 86fa77f..d01e4eb 100644 --- a/docker/x86_64/mingw64 +++ b/docker/x86_64/mingw64 @@ -924,6 +924,7 @@ RUN if [ -f "/3rd_party/rocksdb-${MY_ROCKSDB_VERSION}.tar.gz" ]; then \ -DROCKSDB_INSTALL_ON_WINDOWS=ON \ -DWITH_BENCHMARK=OFF \ -DWITH_BENCHMARK_TOOLS=OFF \ + -DWITH_BZ2=OFF \ -DWITH_CORE_TOOLS=OFF \ -DWITH_EXAMPLES=OFF \ -DWITH_GFLAGS=OFF \ @@ -1169,6 +1170,8 @@ ARG UID=1000 ARG GID=1000 ARG USERNAME=myuser +RUN delgroup scanner || echo "no scanner group found" + RUN addgroup -g $GID $USERNAME && \ adduser -D -u $UID -G $USERNAME -h /home/$USERNAME $USERNAME diff --git a/monitarr/version.cpp.in b/monitarr/version.cpp.in index 2b67e5b..5bd6a55 100644 --- a/monitarr/version.cpp.in +++ b/monitarr/version.cpp.in @@ -11,4 +11,4 @@ namespace monitarr { auto project_get_git_rev() -> std::string_view { return git_rev; } auto project_get_version() -> std::string_view { return version; } -} // namespace %PROJECT_NAME % +} // namespace monitarr diff --git a/scripts/copy_mingw64_deps.sh b/scripts/copy_mingw64_deps.sh index ab905e7..8f44e5c 100755 --- a/scripts/copy_mingw64_deps.sh +++ b/scripts/copy_mingw64_deps.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_MINGW64_COPY_DEPENDENCIES=() diff --git a/scripts/create_containers.sh b/scripts/create_containers.sh index 78155f7..2d785f2 100755 --- a/scripts/create_containers.sh +++ b/scripts/create_containers.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_SCRIPTS_DIR=$(realpath "$0") PROJECT_SCRIPTS_DIR=$(dirname "${PROJECT_SCRIPTS_DIR}") diff --git a/scripts/deliver.sh b/scripts/deliver.sh index 594611e..4d54f61 100755 --- a/scripts/deliver.sh +++ b/scripts/deliver.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash DEST_DIR=$1 DIST_DIR=$2 @@ -35,7 +35,7 @@ fi pushd "${PROJECT_SOURCE_DIR}" BRANCH=$(git branch --show-current) -RELEASE=$(grep PROJECT_RELEASE_ITER= ./config.sh | sed s/PROJECT_RELEASE_ITER=//g) +RELEASE=$(grep PROJECT_RELEASE_ITER= ./config.sh | ${SED} s/PROJECT_RELEASE_ITER=//g) popd if [ "${BRANCH}" == "master" ] || [ "${BRANCH}" == "alpha" ] || @@ -78,14 +78,26 @@ if [ "${PROJECT_IS_MINGW}" == "1" ] && [ -f "${PROJECT_DIST_DIR}/${PROJECT_FILE_ error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}" 1 cp -f "${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}_setup.exe.sha256" ${DEST_DIR} || - error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.sha256" 1 + error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}_setup.exe.sha256" 1 if [ "${PROJECT_PRIVATE_KEY}" != "" ]; then cp -f "${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}_setup.exe.sig" ${DEST_DIR} || - error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.sig" 1 + error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}_setup.exe.sig" 1 fi fi +if [ "${PROJECT_IS_DARWIN}" == "1" ] && [ -f "${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.dmg" ]; then + cp -f -X "${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.dmg" ${DEST_DIR} || + error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.dmg" 1 + + cp -f -X "${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.dmg.sha256" ${DEST_DIR} || + error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.dmg.sha256" 1 + + if [ "${PROJECT_PRIVATE_KEY}" != "" ]; then + cp -f -X "${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.dmg.sig" ${DEST_DIR} || + error_exit "failed to deliver file: ${PROJECT_DIST_DIR}/${PROJECT_FILE_PART}.dmg.sig" 1 + fi +fi popd error_exit "delivered ${PROJECT_FILE_PART}" 0 diff --git a/scripts/docker_common.sh b/scripts/docker_common.sh index d0b58ca..ae0559a 100755 --- a/scripts/docker_common.sh +++ b/scripts/docker_common.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ "${PROJECT_BUILD_ARCH}" == "aarch64" ] && [ "${PROJECT_ENABLE_MULTIARCH_DOCKER}" == "1" ]; then diff --git a/scripts/env.sh b/scripts/env.sh index 638932d..7166d2a 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_BUILD_ARCH=$1 PROJECT_CMAKE_BUILD_TYPE=$2 @@ -56,9 +56,14 @@ PROJECT_MINGW64_COPY_DEPENDENCIES=() PROJECT_MSYS2_PACKAGE_LIST=() PROJECT_REQUIRE_ALPINE=OFF PROJECT_STATIC_LINK=OFF +PROJECT_MACOS_BUNDLE_ID="" +PROJECT_MACOS_ICNS_NAME="" if [ "$(uname -s)" == "Darwin" ]; then PROJECT_IS_DARWIN=1 + export SED=gsed +else + export SED=sed fi if [ "${PROJECT_BUILD_ARCH}" == "" ]; then @@ -238,12 +243,12 @@ if [ "${PROJECT_IS_MINGW}" == "1" ] && [ "${PROJECT_IS_MINGW_UNIX}" == "1" ]; th fi if [ -f "${PROJECT_SOURCE_DIR}/cmake/versions.cmake" ]; then - VERSIONS=($(sed -e s/\ /=/g -e s/set\(//g -e s/\)//g "${PROJECT_SOURCE_DIR}/cmake/versions.cmake")) + VERSIONS=($(${SED} -e s/\ /=/g -e s/set\(//g -e s/\)//g "${PROJECT_SOURCE_DIR}/cmake/versions.cmake")) PROJECT_MINGW64_DOCKER_BUILD_ARGS=() for VERSION in "${VERSIONS[@]}"; do - LOOKUP_NAME=$(echo ${VERSION} | sed s/_VERSION.*// | sed s/GTEST/TESTING/g) + LOOKUP_NAME=$(echo ${VERSION} | ${SED} s/_VERSION.*// | sed s/GTEST/TESTING/g) ENABLE_NAME=PROJECT_ENABLE_${LOOKUP_NAME} if [ "${!ENABLE_NAME}" != "OFF" ]; then PROJECT_MINGW64_DOCKER_BUILD_ARGS+=("--build-arg ${VERSION}") @@ -268,6 +273,8 @@ PROJECT_CMAKE_OPTS="-DPROJECT_IS_ARM64=${PROJECT_IS_ARM64} ${PROJECT_CMAKE_OPTS} PROJECT_CMAKE_OPTS="-DPROJECT_IS_DARWIN=${PROJECT_IS_DARWIN} ${PROJECT_CMAKE_OPTS}" PROJECT_CMAKE_OPTS="-DPROJECT_IS_MINGW=${PROJECT_IS_MINGW} ${PROJECT_CMAKE_OPTS}" PROJECT_CMAKE_OPTS="-DPROJECT_IS_MINGW_UNIX=${PROJECT_IS_MINGW_UNIX} ${PROJECT_CMAKE_OPTS}" +PROJECT_CMAKE_OPTS="-DPROJECT_MACOS_BUNDLE_ID=${PROJECT_MACOS_BUNDLE_ID} ${PROJECT_CMAKE_OPTS}" +PROJECT_CMAKE_OPTS="-DPROJECT_MACOS_ICNS_NAME=${PROJECT_MACOS_ICNS_NAME} ${PROJECT_CMAKE_OPTS}" PROJECT_CMAKE_OPTS="-DPROJECT_MAJOR_VERSION=${PROJECT_MAJOR_VERSION} ${PROJECT_CMAKE_OPTS}" PROJECT_CMAKE_OPTS="-DPROJECT_MINOR_VERSION=${PROJECT_MINOR_VERSION} ${PROJECT_CMAKE_OPTS}" PROJECT_CMAKE_OPTS="-DPROJECT_NAME=${PROJECT_NAME} ${PROJECT_CMAKE_OPTS}" @@ -346,6 +353,8 @@ export PROJECT_IS_DARWIN export PROJECT_IS_MINGW export PROJECT_IS_MINGW_UNIX export PROJECT_LINK_TYPE +export PROJECT_MACOS_BUNDLE_ID +export PROJECT_MACOS_ICNS_NAME export PROJECT_MAJOR_VERSION export PROJECT_MINGW64_COPY_DEPENDENCIES export PROJECT_MINGW64_DOCKER_BUILD_ARGS @@ -403,6 +412,10 @@ echo " Link type: ${PROJECT_LINK_TYPE}" if [ "${PROJECT_IS_MINGW}" == "1" ]; then echo " Long path names: ${PROJECT_ENABLE_WIN32_LONG_PATH_NAMES}" fi +if [ "${PROJECT_IS_DARWIN}" == "1" ]; then + echo " macOS bundle ID: ${PROJECT_MACOS_BUNDLE_ID}" + echo " macOS icns name: ${PROJECT_MACOS_ICNS_NAME}" +fi echo " Meson toolchain file: ${PROJECT_TOOLCHAIN_FILE_MESON}" if [ "${PROJECT_IS_MINGW}" == "1" ] && [ "${PROJECT_IS_MINGW_UNIX}" == "1" ]; then echo " MinGW docker build args: ${PROJECT_MINGW64_DOCKER_BUILD_ARGS}" diff --git a/scripts/info.sh b/scripts/info.sh index 9dd8dbf..6cbfc6f 100755 --- a/scripts/info.sh +++ b/scripts/info.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_SCRIPTS_DIR=$(realpath "$0") PROJECT_SCRIPTS_DIR=$(dirname "${PROJECT_SCRIPTS_DIR}") diff --git a/scripts/libraries.sh b/scripts/libraries.sh index 42938b7..9046804 100755 --- a/scripts/libraries.sh +++ b/scripts/libraries.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_LIBRARIES=( BACKWARD_CPP diff --git a/scripts/make_common.sh b/scripts/make_common.sh index e8b864b..f4ae5b9 100755 --- a/scripts/make_common.sh +++ b/scripts/make_common.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_SCRIPTS_DIR=$(realpath "$0") PROJECT_SCRIPTS_DIR=$(dirname "${PROJECT_SCRIPTS_DIR}") @@ -61,13 +61,27 @@ fi for APP in ${PROJECT_APP_LIST[@]}; do if [ "${PROJECT_BUILD_SHARED_LIBS}" == "ON" ]; then - if [ "${PROJECT_IS_MINGW}" != "1" ]; then + if [ "${PROJECT_IS_MINGW}" == "1" ]; then + rsync -av --progress "${PROJECT_BUILD_DIR}/build/${APP}${PROJECT_APP_BINARY_EXT}" "${PROJECT_DIST_DIR}/" + else rm "${PROJECT_DIST_DIR}/${APP}${PROJECT_APP_BINARY_EXT}" if [ "${PROJECT_ENABLE_CURL}" == "ON" ]; then mv "${PROJECT_DIST_DIR}/cacert.pem" "${PROJECT_DIST_DIR}/bin/cacert.pem" fi rsync -av --progress "${PROJECT_BUILD_DIR}/build/${APP}${PROJECT_APP_BINARY_EXT}" "${PROJECT_DIST_DIR}/bin/" - cat <>"${PROJECT_DIST_DIR}/${APP}${PROJECT_APP_BINARY_EXT}" + if [ "${PROJECT_IS_DARWIN}" == "1" ]; then + cat <>"${PROJECT_DIST_DIR}/${APP}${PROJECT_APP_BINARY_EXT}" +#!/bin/sh +PROJECT_SCRIPTS_DIR=\$(realpath "\$0") +PROJECT_SCRIPTS_DIR=\$(dirname "\${PROJECT_SCRIPTS_DIR}") + +DYLD_LIBRARY_PATH="\${PROJECT_SCRIPTS_DIR}/lib:\${PROJECT_SCRIPTS_DIR}/lib64:\${DYLD_LIBRARY_PATH}" +export DYLD_LIBRARY_PATH + +\${PROJECT_SCRIPTS_DIR}/bin/${APP}${PROJECT_APP_BINARY_EXT} \$* +EOF + else + cat <>"${PROJECT_DIST_DIR}/${APP}${PROJECT_APP_BINARY_EXT}" #!/bin/sh PROJECT_SCRIPTS_DIR=\$(realpath "\$0") PROJECT_SCRIPTS_DIR=\$(dirname "\${PROJECT_SCRIPTS_DIR}") @@ -76,18 +90,28 @@ export LD_LIBRARY_PATH="\${PROJECT_SCRIPTS_DIR}/lib:\${PROJECT_SCRIPTS_DIR}/lib6 \${PROJECT_SCRIPTS_DIR}/bin/${APP}${PROJECT_APP_BINARY_EXT} \$* EOF + fi chmod +x "${PROJECT_DIST_DIR}/${APP}${PROJECT_APP_BINARY_EXT}" - else - rsync -av --progress "${PROJECT_BUILD_DIR}/build/${APP}${PROJECT_APP_BINARY_EXT}" "${PROJECT_DIST_DIR}/" fi - else + elif [ ! -d "${PROJECT_BUILD_DIR}/build/${APP}.app" ]; then rsync -av --progress "${PROJECT_BUILD_DIR}/build/${APP}${PROJECT_APP_BINARY_EXT}" "${PROJECT_DIST_DIR}/" fi done +if [ -d "${PROJECT_BUILD_DIR}/build/${PROJECT_NAME}.app" ]; then + rsync -av --progress "${PROJECT_BUILD_DIR}/build/${PROJECT_NAME}.app/" \ + "${PROJECT_DIST_DIR}/${PROJECT_NAME}.app/" +fi + if [ -f "${PROJECT_SOURCE_DIR}/web/${PROJECT_NAME}/pubspec.yaml" ]; then - rsync -av --progress "${PROJECT_SOURCE_DIR}/web/${PROJECT_NAME}/build/web/" \ - "${PROJECT_DIST_DIR}/web/" + if [ -d "${PROJECT_DIST_DIR}/${PROJECT_NAME}.app" ]; then + rsync -av --progress "${PROJECT_SOURCE_DIR}/web/${PROJECT_NAME}/build/web/" \ + "${PROJECT_DIST_DIR}/${PROJECT_NAME}.app/Contents/Resources/web/" + cp "${PROJECT_DIST_DIR}/cacert.pem" "${PROJECT_DIST_DIR}/${PROJECT_NAME}.app/Contents/MacOS/cacert.pem" + else + rsync -av --progress "${PROJECT_SOURCE_DIR}/web/${PROJECT_NAME}/build/web/" \ + "${PROJECT_DIST_DIR}/web/" + fi fi if [ "${PROJECT_IS_MINGW}" == "1" ]; then diff --git a/scripts/make_flutter.sh b/scripts/make_flutter.sh index 0165e60..3b9973a 100755 --- a/scripts/make_flutter.sh +++ b/scripts/make_flutter.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_SCRIPTS_DIR=$(realpath "$0") PROJECT_SCRIPTS_DIR=$(dirname "${PROJECT_SCRIPTS_DIR}") diff --git a/scripts/make_package.sh b/scripts/make_package.sh index a6e0ac4..eb027ef 100755 --- a/scripts/make_package.sh +++ b/scripts/make_package.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash TEMP_DIR=$(mktemp -d) @@ -12,6 +12,24 @@ function error_exit() { exit $2 } +function cleanup_packages() { + local SOURCE_FILE=$1 + + pushd "${PROJECT_DIST_DIR}" + if [ -f "${SOURCE_FILE}" ]; then + rm -f "${SOURCE_FILE}" || error_exit "failed to delete file: ${SOURCE_FILE}" 1 + fi + + if [ -f "${SOURCE_FILE}.sha256" ]; then + rm -f "${SOURCE_FILE}.sha256" || error_exit "failed to delete file: ${SOURCE_FILE}.sha256" 1 + fi + + if [ -f "${SOURCE_FILE}.sig" ]; then + rm -f "${SOURCE_FILE}.sig" || error_exit "failed to delete file: ${SOURCE_FILE}.sig" 1 + fi + popd +} + function create_file_validations() { local SOURCE_FILE=$1 sha256sum ${SOURCE_FILE} >${SOURCE_FILE}.sha256 || error_exit "failed to create sha256 for file: ${SOURCE_FILE}" 1 @@ -25,42 +43,26 @@ if [ ! -d "${PROJECT_DIST_DIR}" ]; then error_exit "dist directory not found: ${PROJECT_DIST_DIR}" 2 fi -pushd "${PROJECT_DIST_DIR}" -if [ -f "${PROJECT_OUT_FILE}" ]; then - rm -f "${PROJECT_OUT_FILE}" || error_exit "failed to delete file: ${PROJECT_OUT_FILE}" 1 -fi -if [ -f "${PROJECT_OUT_FILE}.sha256" ]; then - rm -f "${PROJECT_OUT_FILE}.sha256" || error_exit "failed to delete file: ${PROJECT_OUT_FILE}.sha256" 1 -fi -if [ -f "${PROJECT_OUT_FILE}.sig" ]; then - rm -f "${PROJECT_OUT_FILE}.sig" || error_exit "failed to delete file: ${PROJECT_OUT_FILE}.sig" 1 -fi - -if [ -f "${PROJECT_FILE_PART}_setup.exe" ]; then - rm -f "${PROJECT_FILE_PART}_setup.exe" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe" 1 -fi -if [ -f "${PROJECT_FILE_PART}_setup.exe.sha256" ]; then - rm -f "${PROJECT_FILE_PART}_setup.exe.sha256" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe.sha256" 1 -fi -if [ -f "${PROJECT_FILE_PART}_setup.exe.sig" ]; then - rm -f "${PROJECT_FILE_PART}_setup.exe.sig" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe.sig" 1 -fi -popd +cleanup_packages "${PROJECT_OUT_FILE}" +cleanup_packages "${PROJECT_FILE_PART}_setup.exe" +cleanup_packages "${PROJECT_FILE_PART}.dmg" rsync -av --progress ${PROJECT_DIST_DIR}/ ${TEMP_DIR}/${PROJECT_NAME}/ || error_exit "failed to rsync" 1 for APP in ${PROJECT_APP_LIST[@]}; do - if [ "${PROJECT_BUILD_SHARED_LIBS}" == "ON" ] && [ "${PROJECT_IS_MINGW}" != "1" ]; then - strip ${TEMP_DIR}/${PROJECT_NAME}/bin/${APP}${PROJECT_APP_BINARY_EXT} + if [ -d "${TEMP_DIR}/${PROJECT_NAME}/${APP}.app" ]; then + strip "${TEMP_DIR}/${PROJECT_NAME}/${APP}.app/Contents/MacOS/${APP}${PROJECT_APP_BINARY_EXT}" + elif [ "${PROJECT_BUILD_SHARED_LIBS}" == "ON" ] && [ "${PROJECT_IS_MINGW}" != "1" ]; then + strip "${TEMP_DIR}/${PROJECT_NAME}/bin/${APP}${PROJECT_APP_BINARY_EXT}" else - strip ${TEMP_DIR}/${PROJECT_NAME}/${APP}${PROJECT_APP_BINARY_EXT} + strip "${TEMP_DIR}/${PROJECT_NAME}/${APP}${PROJECT_APP_BINARY_EXT}" fi done pushd "${TEMP_DIR}/${PROJECT_NAME}/" IFS=$'\n' set -f -FILE_LIST=$(find . -type f) +FILE_LIST=$(find . -type f -not -path "*/.app/*") for FILE in ${FILE_LIST}; do create_file_validations "${FILE}" done @@ -73,7 +75,63 @@ tar cvzf "${PROJECT_OUT_FILE}" -C ${TEMP_DIR} . || error_exit "failed to create create_file_validations "${PROJECT_OUT_FILE}" popd -if [ "${PROJECT_IS_MINGW}" == "1" ] && [ -f "${PROJECT_DIST_DIR}/../${PROJECT_NAME}.iss" ]; then +if [ -d "${TEMP_DIR}/${PROJECT_NAME}/${PROJECT_NAME}.app" ]; then + APP_SRC="${TEMP_DIR}/${PROJECT_NAME}/${PROJECT_NAME}.app" + + DMG_ROOT="${TEMP_DIR}/dmgroot" + mkdir -p "${DMG_ROOT}" || error_exit "failed to create dmgroot" 1 + + INSTALLER="${DMG_ROOT}/Install ${PROJECT_NAME}.command" + INSTALLER_SRC="${PROJECT_SOURCE_DIR}/${PROJECT_NAME}/Install ${PROJECT_NAME}.command" + + if [ -f "${INSTALLER_SRC}" ]; then + HIDDEN_DIR="${DMG_ROOT}/.payload" + mkdir -p "${HIDDEN_DIR}" || error_exit "failed to create payload dir" 1 + APP_DEST_DIR="${HIDDEN_DIR}" + else + APP_DEST_DIR="${DMG_ROOT}" + fi + + rsync -a "${APP_SRC}" "${APP_DEST_DIR}/" || error_exit "failed to stage app bundle" 1 + + if [ -f "${INSTALLER_SRC}" ]; then + chflags hidden "${HIDDEN_DIR}" "${HIDDEN_DIR}/${PROJECT_NAME}.app" 2>/dev/null || true + fi + + ln -s /Applications "${DMG_ROOT}/Applications" 2>/dev/null || true + + if [ -f "${INSTALLER_SRC}" ]; then + cp -f "${INSTALLER_SRC}" "${INSTALLER}" || error_exit "failed to copy install command" 1 + chmod +x "${INSTALLER}" || error_exit "failed to chmod install command" 1 + + SAFE_PREFIX="$(printf '%s' "${PROJECT_MACOS_BUNDLE_ID}" | sed -e 's/[\/&]/\\&/g')" + /usr/bin/sed -i '' -e "s|^LABEL_PREFIX=.*$|LABEL_PREFIX=\"${SAFE_PREFIX}\"|g" "${INSTALLER}" + + LABEL_ASSIGNED="$(/usr/bin/awk -F= '/^LABEL_PREFIX=/{sub(/^[^=]*=/,""); gsub(/^"|"$/,""); print; exit}' "${INSTALLER}")" + if ! /usr/bin/awk -v v="${LABEL_ASSIGNED}" ' + BEGIN { + if (length(v) == 0) exit 1; + if (v == "__LABEL_PREFIX__") exit 1; + if (v !~ /^[A-Za-z0-9._-]+$/) exit 1; + if (v !~ /\./) exit 1; + exit 0; + }'; then + error_exit "DMG build abort: invalid LABEL_PREFIX written to installer (value: \"${LABEL_ASSIGNED}\"). Check PROJECT_MACOS_BUNDLE_ID and sed substitution." 1 + fi + fi + + DMG_OUT="${PROJECT_FILE_PART}.dmg" + hdiutil create \ + -volname "${PROJECT_NAME}" \ + -fs HFS+ \ + -srcfolder "${DMG_ROOT}" \ + -ov -format UDZO \ + "${PROJECT_DIST_DIR}/${DMG_OUT}" || error_exit "hdiutil failed" 1 + + pushd "${PROJECT_DIST_DIR}" + create_file_validations "${DMG_OUT}" + popd +elif [ "${PROJECT_IS_MINGW}" == "1" ] && [ -f "${PROJECT_DIST_DIR}/../${PROJECT_NAME}.iss" ]; then cp -f "${PROJECT_DIST_DIR}/../${PROJECT_NAME}.iss" "${TEMP_DIR}/${PROJECT_NAME}.iss" rsync -av --progress --delete ${PROJECT_SOURCE_DIR}/support/3rd_party/*.msi ${TEMP_DIR}/3rd_party/ diff --git a/scripts/make_unix.sh b/scripts/make_unix.sh index d45bab3..4327c82 100755 --- a/scripts/make_unix.sh +++ b/scripts/make_unix.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_SCRIPTS_DIR=$(realpath "$0") PROJECT_SCRIPTS_DIR=$(dirname "${PROJECT_SCRIPTS_DIR}") diff --git a/scripts/make_win32.sh b/scripts/make_win32.sh index b90b276..009e7ad 100755 --- a/scripts/make_win32.sh +++ b/scripts/make_win32.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_SCRIPTS_DIR=$(realpath "$0") PROJECT_SCRIPTS_DIR=$(dirname "${PROJECT_SCRIPTS_DIR}") diff --git a/scripts/run_docker_shell.sh b/scripts/run_docker_shell.sh index c2d88aa..8a08db1 100755 --- a/scripts/run_docker_shell.sh +++ b/scripts/run_docker_shell.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash DOCKER_NAME=$1 diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 03d55c8..4f2483f 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_SCRIPTS_DIR=$(realpath "$0") PROJECT_SCRIPTS_DIR=$(dirname "${PROJECT_SCRIPTS_DIR}") diff --git a/scripts/setup_msys2.sh b/scripts/setup_msys2.sh index 7a91c90..bc1fab0 100755 --- a/scripts/setup_msys2.sh +++ b/scripts/setup_msys2.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PROJECT_MSYS2_PACKAGE_LIST=() diff --git a/scripts/versions.sh b/scripts/versions.sh index 01a8e58..b59002f 100755 --- a/scripts/versions.sh +++ b/scripts/versions.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash declare -A PROJECT_VERSIONS PROJECT_VERSIONS[BINUTILS]="2.44" diff --git a/support/3rd_party/icu_configure.sh b/support/3rd_party/icu_configure.sh new file mode 100644 index 0000000..0dcaa5f --- /dev/null +++ b/support/3rd_party/icu_configure.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +if [ "$(uname -m)" == "arm64" ] && + [ "${PROJECT_IS_ARM64}" == "0" ]; then + HOST_CFG="--host=x86_64-apple-darwin" + export CC="clang -arch x86_64" + export CXX="clang++ -arch x86_64" +fi + +CXXFLAGS="-std=gnu++17 -march=$1 -mtune=generic" ./configure \ + --disable-samples \ + --disable-tests \ + --enable-shared=$3 \ + --enable-static=yes \ + --prefix="$2" \ + ${HOST_CFG} diff --git a/support/include/utils/all.hpp b/support/include/utils/all.hpp index 4a7851f..ad94342 100644 --- a/support/include/utils/all.hpp +++ b/support/include/utils/all.hpp @@ -24,6 +24,7 @@ #include "utils/config.hpp" +#include "utils/atomic.hpp" #include "utils/base64.hpp" #include "utils/collection.hpp" #if defined(_WIN32) @@ -49,6 +50,7 @@ #include "utils/path.hpp" #include "utils/string.hpp" #include "utils/time.hpp" +#include "utils/ttl_cache.hpp" #if !defined(_WIN32) #include "utils/unix.hpp" #endif // !defined(_WIN32) diff --git a/support/include/utils/atomic.hpp b/support/include/utils/atomic.hpp new file mode 100644 index 0000000..34a9a60 --- /dev/null +++ b/support/include/utils/atomic.hpp @@ -0,0 +1,118 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef MONITARR_INCLUDE_UTILS_ATOMIC_HPP_ +#define MONITARR_INCLUDE_UTILS_ATOMIC_HPP_ + +#include "utils/config.hpp" + +namespace monitarr::utils { +template class atomic final { +public: + atomic() : mtx_(std::make_shared()) {} + + atomic(const atomic &at_data) + : data_(at_data.load()), mtx_(std::make_shared()) {} + + atomic(data_t data) + : data_(std::move(data)), mtx_(std::make_shared()) {} + + atomic(atomic &&) = default; + + ~atomic() = default; + +private: + data_t data_; + std::shared_ptr mtx_; + +public: + [[nodiscard]] auto load() const -> data_t { + mutex_lock lock(*mtx_); + return data_; + } + + auto store(data_t data) -> data_t { + mutex_lock lock(*mtx_); + data_ = std::move(data); + return data_; + } + + auto operator=(const atomic &at_data) -> atomic & { + if (&at_data == this) { + return *this; + } + + store(at_data.load()); + return *this; + } + + auto operator=(atomic &&) -> atomic & = default; + + auto operator=(data_t data) -> atomic & { + if (&data == &data_) { + return *this; + } + + store(std::move(data)); + return *this; + } + + [[nodiscard]] auto operator==(const atomic &at_data) const -> bool { + if (&at_data == this) { + return true; + } + + mutex_lock lock(*mtx_); + return at_data.load() == data_; + } + + [[nodiscard]] auto operator==(const data_t &data) const -> bool { + if (&data == &data_) { + return true; + } + + mutex_lock lock(*mtx_); + return data == data_; + } + + [[nodiscard]] auto operator!=(const atomic &at_data) const -> bool { + if (&at_data == this) { + return false; + } + + mutex_lock lock(*mtx_); + return at_data.load() != data_; + } + + [[nodiscard]] auto operator!=(const data_t &data) const -> bool { + if (&data == &data_) { + return false; + } + + mutex_lock lock(*mtx_); + return data != data_; + } + + [[nodiscard]] operator data_t() const { return load(); } +}; +} // namespace monitarr::utils + +#endif // MONITARR_INCLUDE_UTILS_ATOMIC_HPP_ diff --git a/support/include/utils/base64.hpp b/support/include/utils/base64.hpp index 2c63569..02e07c3 100644 --- a/support/include/utils/base64.hpp +++ b/support/include/utils/base64.hpp @@ -1,10 +1,11 @@ // NOLINTBEGIN -#ifndef _MACARON_BASE64_H_ -#define _MACARON_BASE64_H_ +#ifndef MACARON_BASE64_H_ +#define MACARON_BASE64_H_ /** * The MIT License (MIT) * Copyright (c) 2016 tomykaira + * Copyright (c) 2025 scott.e.graves@protonmail.com * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -39,121 +40,272 @@ #endif #include +#include #include +#include #include namespace macaron::Base64 { -static std::string Encode(const unsigned char *data, std::size_t len) { - static constexpr std::array sEncodingTable{ - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', - 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', - }; - auto in_len{len}; - std::string ret; - if (in_len > 0) { - std::size_t out_len{4U * ((in_len + 2U) / 3U)}; - ret = std::string(out_len, '\0'); - std::size_t i; - auto *p = reinterpret_cast(ret.data()); +// --- Alphabets -------------------------------------------------------------- - for (i = 0U; i < in_len - 2U; i += 3U) { - *p++ = sEncodingTable[(data[i] >> 2U) & 0x3F]; - *p++ = sEncodingTable[((data[i] & 0x3) << 4U) | - ((int)(data[i + 1U] & 0xF0) >> 4U)]; - *p++ = sEncodingTable[((data[i + 1] & 0xF) << 2) | - ((int)(data[i + 2U] & 0xC0) >> 6U)]; - *p++ = sEncodingTable[data[i + 2U] & 0x3F]; +static constexpr std::array kStdAlphabet{ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', +}; + +static constexpr std::array kUrlAlphabet{ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', +}; + +// Decoding table that accepts BOTH standard and URL-safe alphabets. +static constexpr std::array kDecodingTable = [] { + std::array t{}; + t.fill(64U); + // 'A'-'Z' + for (unsigned char c = 'A'; c <= 'Z'; ++c) + t[c] = static_cast(c - 'A'); + // 'a'-'z' + for (unsigned char c = 'a'; c <= 'z'; ++c) + t[c] = static_cast(26 + c - 'a'); + // '0'-'9' + for (unsigned char c = '0'; c <= '9'; ++c) + t[c] = static_cast(52 + c - '0'); + // Standard extras + t[static_cast('+')] = 62U; + t[static_cast('/')] = 63U; + // URL-safe extras + t[static_cast('-')] = 62U; + t[static_cast('_')] = 63U; + return t; +}(); + +// --- Encoding --------------------------------------------------------------- + +/** + * Encode to Base64. + * @param data pointer to bytes + * @param len number of bytes + * @param url_safe if true, use URL-safe alphabet ("-","_") instead of ("+","/") + * @param pad if true, add '=' padding; if false, omit padding (RFC 4648 + * §5) + */ +static std::string Encode(const unsigned char *data, std::size_t len, + bool url_safe = false, bool pad = true) { + const auto &alpha = url_safe ? kUrlAlphabet : kStdAlphabet; + + std::string out; + if (len == 0U) { + return out; + } + + const std::size_t full_blocks = len / 3U; + const std::size_t rem = len % 3U; + + std::size_t out_len{}; + if (pad) { + out_len = 4U * ((len + 2U) / 3U); + } else { + // Unpadded length per RFC 4648 §5 + out_len = 4U * full_blocks + (rem == 0U ? 0U : (rem == 1U ? 2U : 3U)); + } + out.assign(out_len, '\0'); + + auto *p = reinterpret_cast(out.data()); + std::size_t i = 0; + + // Full 3-byte blocks -> 4 chars + for (; i + 2U < len; i += 3U) { + const unsigned char b0 = data[i + 0U]; + const unsigned char b1 = data[i + 1U]; + const unsigned char b2 = data[i + 2U]; + + *p++ = alpha[(b0 >> 2U) & 0x3F]; + *p++ = alpha[((b0 & 0x03U) << 4U) | ((b1 >> 4U) & 0x0FU)]; + *p++ = alpha[((b1 & 0x0FU) << 2U) | ((b2 >> 6U) & 0x03U)]; + *p++ = alpha[b2 & 0x3FU]; + } + + // Remainder + if (rem == 1U) { + const unsigned char b0 = data[i]; + *p++ = alpha[(b0 >> 2U) & 0x3F]; + *p++ = alpha[(b0 & 0x03U) << 4U]; + if (pad) { + *p++ = '='; + *p++ = '='; } - if (i < in_len) { - *p++ = sEncodingTable[(data[i] >> 2U) & 0x3F]; - if (i == (in_len - 1U)) { - *p++ = sEncodingTable[((data[i] & 0x3) << 4U)]; - *p++ = '='; - } else { - *p++ = sEncodingTable[((data[i] & 0x3) << 4U) | - ((int)(data[i + 1U] & 0xF0) >> 4U)]; - *p++ = sEncodingTable[((data[i + 1U] & 0xF) << 2U)]; - } + } else if (rem == 2U) { + const unsigned char b0 = data[i + 0U]; + const unsigned char b1 = data[i + 1U]; + *p++ = alpha[(b0 >> 2U) & 0x3F]; + *p++ = alpha[((b0 & 0x03U) << 4U) | ((b1 >> 4U) & 0x0FU)]; + *p++ = alpha[(b1 & 0x0FU) << 2U]; + if (pad) { *p++ = '='; } } - return ret; + return out; } -[[maybe_unused]] static std::string Encode(std::string_view data) { +[[maybe_unused]] static std::string +Encode(std::string_view data, bool url_safe = false, bool pad = true) { return Encode(reinterpret_cast(data.data()), - data.size()); + data.size(), url_safe, pad); } +[[maybe_unused]] static std::string +EncodeUrlSafe(const unsigned char *data, std::size_t len, bool pad = false) { + return Encode(data, len, /*url_safe=*/true, /*pad=*/pad); +} + +[[maybe_unused]] static std::string EncodeUrlSafe(std::string_view data, + bool pad = false) { + return Encode(reinterpret_cast(data.data()), + data.size(), /*url_safe=*/true, /*pad=*/pad); +} + +// --- Decoding --------------------------------------------------------------- + +/** + * Decode standard OR URL-safe Base64. + * Accepts inputs with or without '=' padding. + * Throws std::runtime_error on malformed input. + */ [[maybe_unused]] static std::vector Decode(std::string_view input) { - static constexpr std::array kDecodingTable{ - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, 52, 53, 54, 55, 56, 57, - 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6, - 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, - 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, + std::vector out; + if (input.empty()) { + return out; + } + + std::size_t inLen = input.size(); + std::size_t rem = inLen % 4U; + + // padded if multiple of 4 and last char is '=' + bool hasPadding = (rem == 0U) && (inLen >= 4U) && (input[inLen - 1U] == '='); + + // compute output length + std::size_t outLen{}; + if (hasPadding) { + outLen = (inLen / 4U) * 3U; + if (input[inLen - 1U] == '=') + outLen--; + if (input[inLen - 2U] == '=') + outLen--; + } else { + if (rem == 1U) { + throw std::runtime_error("Invalid Base64 length (mod 4 == 1)"); + } + outLen = (inLen / 4U) * 3U + (rem == 0U ? 0U : (rem == 2U ? 1U : 2U)); + } + + out.resize(outLen); + + auto readVal = [](unsigned char c) -> unsigned char { + unsigned char v = kDecodingTable[c]; + if (v == 64U) { + throw std::runtime_error("Invalid Base64 character"); + } + return v; }; - std::vector out; - if (not input.empty()) { - auto in_len{input.size()}; - if (in_len % 4U != 0U) { - throw std::runtime_error("Input data size is not a multiple of 4"); - } + std::size_t i = 0U; + std::size_t j = 0U; - std::size_t out_len{in_len / 4U * 3U}; - if (input[in_len - 1U] == '=') { - out_len--; - } - if (input[in_len - 2U] == '=') { - out_len--; - } + // process all full unpadded quartets + std::size_t lastFull = + hasPadding ? (inLen - 4U) : (rem == 0U ? inLen : (inLen - rem)); - out.resize(out_len); + while (i + 4U <= lastFull) { + unsigned char a = readVal(static_cast(input[i + 0U])); + unsigned char b = readVal(static_cast(input[i + 1U])); + unsigned char c = readVal(static_cast(input[i + 2U])); + unsigned char d = readVal(static_cast(input[i + 3U])); + i += 4U; - for (std::size_t i = 0U, j = 0U; i < in_len;) { - std::uint32_t a = - input.at(i) == '=' - ? 0U & i++ - : kDecodingTable[static_cast(input.at(i++))]; - std::uint32_t b = - input.at(i) == '=' - ? 0U & i++ - : kDecodingTable[static_cast(input.at(i++))]; - std::uint32_t c = - input.at(i) == '=' - ? 0U & i++ - : kDecodingTable[static_cast(input.at(i++))]; - std::uint32_t d = - input.at(i) == '=' - ? 0U & i++ - : kDecodingTable[static_cast(input.at(i++))]; + std::uint32_t triple = (static_cast(a) << 18U) | + (static_cast(b) << 12U) | + (static_cast(c) << 6U) | + (static_cast(d)); - std::uint32_t triple = - (a << 3U * 6U) + (b << 2U * 6U) + (c << 1U * 6U) + (d << 0U * 6U); + if (j < outLen) + out[j++] = static_cast((triple >> 16U) & 0xFFU); + if (j < outLen) + out[j++] = static_cast((triple >> 8U) & 0xFFU); + if (j < outLen) + out[j++] = static_cast(triple & 0xFFU); + } - if (j < out_len) - out[j++] = (triple >> 2U * 8U) & 0xFF; - if (j < out_len) - out[j++] = (triple >> 1U * 8U) & 0xFF; - if (j < out_len) - out[j++] = (triple >> 0U * 8U) & 0xFF; + // tail: padded quartet or unpadded remainder + if (i < inLen) { + std::size_t left = inLen - i; + + if (left == 4U) { + bool thirdIsPad = (input[i + 2U] == '='); + bool fourthIsPad = (input[i + 3U] == '='); + + // '=' is never allowed in positions 1 or 2 of any quartet + if (input[i + 0U] == '=' || input[i + 1U] == '=') { + throw std::runtime_error("Invalid Base64 padding placement"); + } + + unsigned char a = readVal(static_cast(input[i + 0U])); + unsigned char b = readVal(static_cast(input[i + 1U])); + unsigned char c = 0U; + unsigned char d = 0U; + + if (!thirdIsPad) { + c = readVal(static_cast(input[i + 2U])); + if (!fourthIsPad) { + d = readVal(static_cast(input[i + 3U])); + } + } else { + // if the 3rd is '=', the 4th must also be '=' + if (!fourthIsPad) { + throw std::runtime_error("Invalid Base64 padding placement"); + } + } + i += 4U; + + std::uint32_t triple = (static_cast(a) << 18U) | + (static_cast(b) << 12U) | + (static_cast(c) << 6U) | + (static_cast(d)); + + if (j < outLen) + out[j++] = static_cast((triple >> 16U) & 0xFFU); + if (!thirdIsPad && j < outLen) + out[j++] = static_cast((triple >> 8U) & 0xFFU); + if (!fourthIsPad && !thirdIsPad && j < outLen) + out[j++] = static_cast(triple & 0xFFU); + + } else if (left == 2U || left == 3U) { + unsigned char a = readVal(static_cast(input[i + 0U])); + unsigned char b = readVal(static_cast(input[i + 1U])); + unsigned char c = (left == 3U) + ? readVal(static_cast(input[i + 2U])) + : 0U; + i += left; + + std::uint32_t triple = (static_cast(a) << 18U) | + (static_cast(b) << 12U) | + (static_cast(c) << 6U); + + if (j < outLen) + out[j++] = static_cast((triple >> 16U) & 0xFFU); + if (left == 3U && j < outLen) + out[j++] = static_cast((triple >> 8U) & 0xFFU); + } else { + throw std::runtime_error("Invalid Base64 length (mod 4 == 1)"); } } @@ -169,6 +321,5 @@ Decode(std::string_view input) { #pragma clang diagnostic pop #endif -#endif /* _MACARON_BASE64_H_ */ - -// NOLINTEND +#endif /* MACARON_BASE64_H_ */ + // NOLINTEND diff --git a/support/include/utils/com_init_wrapper.hpp b/support/include/utils/com_init_wrapper.hpp index 12237d9..5c11f31 100644 --- a/support/include/utils/com_init_wrapper.hpp +++ b/support/include/utils/com_init_wrapper.hpp @@ -46,7 +46,7 @@ struct com_init_wrapper final { [[nodiscard]] auto is_initialized() const -> bool { return initialized_; } private: - BOOL initialized_; + BOOL initialized_{}; }; } // namespace monitarr::utils diff --git a/support/include/utils/common.hpp b/support/include/utils/common.hpp index b949921..8567aea 100644 --- a/support/include/utils/common.hpp +++ b/support/include/utils/common.hpp @@ -35,9 +35,10 @@ struct result final { using retryable_action_t = std::function; -[[nodiscard]] inline constexpr auto -calculate_read_size(std::uint64_t total_size, std::size_t read_size, - std::uint64_t offset) -> std::size_t { +[[nodiscard]] constexpr auto calculate_read_size(std::uint64_t total_size, + std::size_t read_size, + std::uint64_t offset) + -> std::size_t { return static_cast( ((offset + read_size) > total_size) ? ((offset < total_size) ? total_size - offset : 0U) diff --git a/support/include/utils/config.hpp b/support/include/utils/config.hpp index d5f8f68..a82a77d 100644 --- a/support/include/utils/config.hpp +++ b/support/include/utils/config.hpp @@ -107,7 +107,6 @@ #include #include #include -#include #include #include #include @@ -149,6 +148,9 @@ #include #endif // defined(__cplusplus) +#include +#include + #if defined(PROJECT_ENABLE_CURL) #include "curl/curl.h" #include "curl/multi.h" @@ -423,6 +425,8 @@ using vlc_string_t = std::unique_ptr; namespace monitarr { using data_buffer = std::vector; +using data_span = std::span; +using data_cspan = std::span; using mutex_lock = std::lock_guard; using recur_mutex_lock = std::lock_guard; using stop_type = std::atomic_bool; diff --git a/support/include/utils/encrypting_reader.hpp b/support/include/utils/encrypting_reader.hpp index 0ed2967..815291f 100644 --- a/support/include/utils/encrypting_reader.hpp +++ b/support/include/utils/encrypting_reader.hpp @@ -25,6 +25,7 @@ #include "utils/config.hpp" +#include "utils/encryption.hpp" #include "utils/hash.hpp" #include "utils/types/file/i_file.hpp" @@ -37,14 +38,74 @@ public: std::optional relative_parent_path, std::size_t error_return = 0U); - encrypting_reader(std::string_view encrypted_file_path, - std::string_view source_path, - stop_type_callback stop_requested_cb, - std::string_view token, std::size_t error_return = 0U); + encrypting_reader(stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, + std::string_view source_path, std::string_view token, + std::size_t error_return = 0U); encrypting_reader( + stop_type_callback stop_requested_cb, std::string_view encrypted_file_path, std::string_view source_path, - stop_type_callback stop_requested_cb, std::string_view token, + std::string_view token, + std::vector> + iv_list, + std::size_t error_return = 0U); + + encrypting_reader(std::string_view file_name, std::string_view source_path, + stop_type_callback stop_requested_cb, + std::string_view token, kdf_config cfg, + std::optional relative_parent_path, + std::size_t error_return = 0U); + + encrypting_reader(stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, + std::string_view source_path, std::string_view token, + kdf_config cfg, std::size_t error_return = 0U); + + encrypting_reader( + stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, std::string_view source_path, + std::string_view token, kdf_config cfg, + std::vector> + iv_list, + std::size_t error_return = 0U); + + encrypting_reader(std::string_view file_name, std::string_view source_path, + stop_type_callback stop_requested_cb, + const utils::hash::hash_256_t &master_key, + const kdf_config &cfg, + std::optional relative_parent_path, + std::size_t error_return = 0U); + + encrypting_reader(std::string_view file_name, std::string_view source_path, + stop_type_callback stop_requested_cb, + const utils::hash::hash_256_t &master_key, + const std::pair &configs, + std::optional relative_parent_path, + std::size_t error_return = 0U); + + encrypting_reader(stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, + std::string_view source_path, + const utils::hash::hash_256_t &master_key, + const kdf_config &cfg, std::size_t error_return = 0U); + + encrypting_reader( + stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, std::string_view source_path, + const utils::hash::hash_256_t &master_key, const kdf_config &cfg, + std::vector> + iv_list, + std::size_t error_return = 0U); + + encrypting_reader( + stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, std::string_view source_path, + const utils::hash::hash_256_t &master_key, + const std::pair &configs, std::vector> iv_list, @@ -60,21 +121,25 @@ public: public: using iostream = std::basic_iostream>; + using kdf_pair_t = std::pair; + using key_pair_t = + std::pair; using streambuf = std::basic_streambuf>; private: - utils::encryption::hash_256_t key_; + key_pair_t keys_; stop_type_callback stop_requested_cb_; size_t error_return_; std::unique_ptr source_file_; - -private: - std::unordered_map chunk_buffers_; std::string encrypted_file_name_; std::string encrypted_file_path_; std::vector< std::array> iv_list_; + +private: + std::unordered_map chunk_buffers_; + std::optional kdf_headers_; std::size_t last_data_chunk_{}; std::size_t last_data_chunk_size_{}; std::uint64_t read_offset_{}; @@ -88,12 +153,30 @@ private: private: auto reader_function(char *buffer, size_t size, size_t nitems) -> size_t; + void common_initialize(bool procces_iv_list); + + void common_initialize_kdf_data(const kdf_config &cfg, + const utils::hash::hash_256_t &master_key); + + void common_initialize_kdf_keys(std::string_view token, kdf_config &cfg); + + void common_initialize_kdf_path(const utils::hash::hash_256_t &master_key); + + void create_encrypted_paths(std::string_view file_name, + std::optional relative_parent_path); + public: - [[nodiscard]] static auto calculate_decrypted_size(std::uint64_t total_size) + [[nodiscard]] static auto calculate_decrypted_size(std::uint64_t total_size, + bool uses_kdf) -> std::uint64_t; [[nodiscard]] static auto - calculate_encrypted_size(std::string_view source_path) -> std::uint64_t; + calculate_encrypted_size(std::string_view source_path, bool uses_kdf) + -> std::uint64_t; + + [[nodiscard]] static auto calculate_encrypted_size(std::uint64_t size, + bool uses_kdf) + -> std::uint64_t; [[nodiscard]] auto create_iostream() const -> std::shared_ptr; @@ -127,6 +210,12 @@ public: return iv_list_; } + [[nodiscard]] auto get_kdf_config_for_data() const + -> std::optional; + + [[nodiscard]] auto get_kdf_config_for_path() const + -> std::optional; + [[nodiscard]] auto get_stop_requested() const -> bool { return stop_requested_cb_(); } diff --git a/support/include/utils/encryption.hpp b/support/include/utils/encryption.hpp index 3e4144d..a62de0a 100644 --- a/support/include/utils/encryption.hpp +++ b/support/include/utils/encryption.hpp @@ -25,6 +25,9 @@ #include "utils/config.hpp" +#if defined(PROJECT_ENABLE_BOOST) && defined(PROJECT_ENABLE_JSON) +#include "utils/collection.hpp" +#endif // defined(PROJECT_ENABLE_BOOST) && defined(PROJECT_ENABLE_JSON) #include "utils/error.hpp" #include "utils/hash.hpp" @@ -34,32 +37,236 @@ inline constexpr std::uint32_t encryption_header_size{ crypto_aead_xchacha20poly1305_IETF_ABYTES, }; +#if defined(PROJECT_ENABLE_BOOST) +enum class kdf_version : std::uint8_t { v1 }; + +enum class kdf_type : std::uint8_t { argon2id }; + +enum class memlimit_level : std::uint8_t { + level1, // 64MiB + level2, // 256MiB + level3, // 512MiB + level4, // 1GiB +}; + +enum class opslimit_level : std::uint8_t { + level1, // interactive + level2, // moderate + level3, // sensitive +}; + +[[nodiscard]] inline auto get_memlimit(memlimit_level memlimit) -> size_t { + constexpr auto mib512{512ULL * 1024ULL * 1024ULL}; + + switch (memlimit) { + case memlimit_level::level1: + return crypto_pwhash_MEMLIMIT_INTERACTIVE; + + case memlimit_level::level2: + return crypto_pwhash_MEMLIMIT_MODERATE; + + case memlimit_level::level3: + return mib512; + + case memlimit_level::level4: + return crypto_pwhash_MEMLIMIT_SENSITIVE; + } + + return mib512; +} + +[[nodiscard]] inline auto get_opslimit(opslimit_level opslimit) + -> unsigned long long { + switch (opslimit) { + case opslimit_level::level1: + return crypto_pwhash_OPSLIMIT_INTERACTIVE; + + case opslimit_level::level2: + return crypto_pwhash_OPSLIMIT_MODERATE; + + case opslimit_level::level3: + return crypto_pwhash_OPSLIMIT_SENSITIVE; + } + + return crypto_pwhash_OPSLIMIT_MODERATE; +} + +enum class kdf_context : std::uint8_t { + data, + path, + undefined, +}; +using kdf_ctx_t = std::array; + +namespace kdf { +constexpr inline std::array< + kdf_ctx_t, static_cast(kdf_context::undefined) + 1U> + KDF_CTXS{ + { + {'D', 'A', 'T', 'A', '_', 'C', 'T', 'X'}, + {'F', 'I', 'L', 'E', '_', 'C', 'T', 'X'}, + {'D', 'E', 'F', 'L', '_', 'C', 'T', 'X'}, + }, + }; +} // namespace kdf + +[[nodiscard]] constexpr inline auto get_kdf_context_name(kdf_context ctx) + -> kdf_ctx_t { + const auto idx = static_cast(ctx); + return idx < kdf::KDF_CTXS.size() ? kdf::KDF_CTXS.at(idx) + : kdf::KDF_CTXS.back(); +} + +#pragma pack(push, 1) +struct kdf_config final { + using salt_t = std::array; + + kdf_version version{kdf_version::v1}; + kdf_type kdf{kdf_type::argon2id}; + memlimit_level memlimit{memlimit_level::level3}; + opslimit_level opslimit{opslimit_level::level2}; + std::uint64_t unique_id{}; + salt_t salt{}; + std::uint64_t checksum{}; + + template + [[nodiscard]] auto create_subkey(kdf_context ctx, std::size_t unique_id_, + const hash_t &master_key) const + -> std::pair { + auto sub_key = derive_subkey(ctx, unique_id_, master_key); + + auto cfg = *this; + cfg.unique_id = unique_id_; + cfg.checksum = cfg.generate_checksum(); + return {sub_key, cfg}; + } + + template + [[nodiscard]] static auto derive_subkey(kdf_context ctx, + std::size_t unique_id_, + const hash_t &master_key) -> hash_t { + MONITARR_USES_FUNCTION_NAME(); + + hash_t sub_key{}; + auto res = crypto_kdf_derive_from_key( + sub_key.data(), sub_key.size(), unique_id_, + get_kdf_context_name(ctx).data(), master_key.data()); + if (res != 0) { + throw monitarr::utils::error::create_exception( + function_name, { + "failed to derive sub-key", + std::to_string(res), + }); + } + + return sub_key; + } + + template + [[nodiscard]] auto recreate_subkey(kdf_context ctx, + const hash_t &master_key) const -> hash_t { + return derive_subkey(ctx, unique_id, master_key); + } + + [[nodiscard]] static auto from_header(data_cspan data, kdf_config &cfg, + bool ignore_checksum = false) -> bool; + + [[nodiscard]] auto generate_checksum() const -> std::uint64_t; + + void seal(); + + [[nodiscard]] static constexpr auto size() -> std::size_t { + return sizeof(kdf_config); + } + + [[nodiscard]] auto to_header() const -> data_buffer; + + [[nodiscard]] auto operator==(const kdf_config &) const -> bool = default; + [[nodiscard]] auto operator!=(const kdf_config &) const -> bool = default; +}; +#pragma pack(pop) +#endif // defined(PROJECT_ENABLE_BOOST) + template -inline auto generate_key( +[[nodiscard]] inline auto generate_key( std::string_view password, std::function hasher = - default_create_hash()) -> hash_t; + utils::hash::default_create_hash()) -> hash_t; template -inline auto generate_key( +[[nodiscard]] inline auto generate_key( std::wstring_view password, std::function hasher = - default_create_hash()) -> hash_t; + utils::hash::default_create_hash()) -> hash_t; #if defined(PROJECT_ENABLE_BOOST) +template +[[nodiscard]] inline auto generate_key(std::string_view password, + kdf_config &cfg) -> hash_t; + +template +[[nodiscard]] inline auto generate_key(std::wstring_view password, + kdf_config &cfg) -> hash_t; + +template +[[nodiscard]] inline auto recreate_key(std::string_view password, + const kdf_config &cfg) -> hash_t; + +template +[[nodiscard]] inline auto recreate_key(std::wstring_view password, + const kdf_config &cfg) -> hash_t; + +template +[[nodiscard]] auto create_key_argon2id(string_t password, kdf_config &cfg, + utils::hash::hash_256_t &key) -> bool; + +template +[[nodiscard]] auto recreate_key_argon2id(string_t password, + const kdf_config &cfg, + utils::hash::hash_256_t &key) -> bool; + +template +[[nodiscard]] inline auto +detect_and_recreate_key(string_t password, data_cspan header, hash_t &key, + std::optional &cfg) -> bool; + +template +[[nodiscard]] inline auto +detect_and_recreate_key(std::string_view password, data_cspan header, + hash_t &key, std::optional &cfg) -> bool; + +template +[[nodiscard]] inline auto +detect_and_recreate_key(std::wstring_view password, data_cspan header, + hash_t &key, std::optional &cfg) -> bool; + [[nodiscard]] auto decrypt_file_name(std::string_view encryption_token, std::string &file_name) -> bool; [[nodiscard]] auto decrypt_file_path(std::string_view encryption_token, std::string &file_path) -> bool; +[[nodiscard]] auto decrypt_file_name(std::string_view encryption_token, + const kdf_config &cfg, + std::string &file_name) -> bool; + +[[nodiscard]] auto decrypt_file_path(std::string_view encryption_token, + const kdf_config &cfg, + std::string &file_path) -> bool; + +[[nodiscard]] auto decrypt_file_name(const utils::hash::hash_256_t &master_key, + std::string &file_name) -> bool; + +[[nodiscard]] auto decrypt_file_path(const utils::hash::hash_256_t &master_key, + std::string &file_path) -> bool; + template [[nodiscard]] inline auto decrypt_data(const std::array &key, const unsigned char *buffer, - std::size_t buffer_size, - result_t &res) -> bool { + std::size_t buffer_size, result_t &res) + -> bool { if (buffer_size > encryption_header_size) { - const std::uint32_t size = + std::uint32_t size = boost::endian::native_to_big(static_cast(buffer_size)); res.resize(buffer_size - encryption_header_size); return crypto_aead_xchacha20poly1305_ietf_decrypt_detached( @@ -76,32 +283,53 @@ template template [[nodiscard]] inline auto decrypt_data(const std::array &key, - const buffer_t &buf, - result_t &res) -> bool { + const buffer_t &buf, result_t &res) + -> bool { return decrypt_data( key, reinterpret_cast(buf.data()), buf.size(), res); } -template +template [[nodiscard]] inline auto decrypt_data( std::string_view password, const buffer_t &buf, result_t &res, std::function hasher = - default_create_hash()) -> bool { + utils::hash::default_create_hash()) -> bool { return decrypt_data(generate_key(password, hasher), buf, res); } -template +template +[[nodiscard]] inline auto decrypt_data(std::string_view password, + const kdf_config &cfg, + const buffer_t &buf, result_t &res) + -> bool { + return decrypt_data(recreate_key(password, cfg), + buf, res); +} + +template [[nodiscard]] inline auto decrypt_data( std::string_view password, const unsigned char *buffer, std::size_t buffer_size, result_t &res, std::function hasher = - default_create_hash()) -> bool { + utils::hash::default_create_hash()) -> bool { return decrypt_data(generate_key(password, hasher), buffer, buffer_size, res); } +template +[[nodiscard]] inline auto decrypt_data(std::string_view password, + const kdf_config &cfg, + const unsigned char *buffer, + std::size_t buffer_size, result_t &res) + -> bool { + return decrypt_data(recreate_key(password, cfg), buffer, + buffer_size, res); +} + template inline void encrypt_data(const std::array &key, encrypt_data(iv, key, buffer, buffer_size, res); } -template +template inline void encrypt_data( std::string_view password, const unsigned char *buffer, std::size_t buffer_size, result_t &res, std::function hasher = - default_create_hash()) { + utils::hash::default_create_hash()) { encrypt_data(generate_key(password, hasher), buffer, buffer_size, res); } -template +template +inline void encrypt_data(std::string_view password, kdf_config &cfg, + const unsigned char *buffer, std::size_t buffer_size, + result_t &res) { + encrypt_data(generate_key(password, cfg), buffer, + buffer_size, res); +} + +template inline void encrypt_data( std::string_view password, const buffer_t &buf, result_t &res, std::function hasher = - default_create_hash()) { + utils::hash::default_create_hash()) { encrypt_data(generate_key(password, hasher), reinterpret_cast(buf.data()), buf.size(), res); } +template +inline void encrypt_data(std::string_view password, kdf_config &cfg, + const buffer_t &buf, result_t &res) { + encrypt_data(generate_key(password, cfg), + reinterpret_cast(buf.data()), + buf.size(), res); +} + template inline void encrypt_data(const std::array &key, @@ -189,16 +435,52 @@ using reader_func_t = std::function; -[[nodiscard]] auto -read_encrypted_range(const http_range &range, - const utils::encryption::hash_256_t &key, - reader_func_t reader_func, std::uint64_t total_size, - data_buffer &data) -> bool; +[[nodiscard]] auto read_encrypted_range(const http_range &range, + const utils::hash::hash_256_t &key, + bool uses_kdf, + reader_func_t reader_func, + std::uint64_t total_size, + data_buffer &data) -> bool; [[nodiscard]] auto read_encrypted_range( - const http_range &range, const utils::encryption::hash_256_t &key, + const http_range &range, const utils::hash::hash_256_t &key, bool uses_kdf, reader_func_t reader_func, std::uint64_t total_size, unsigned char *data, std::size_t size, std::size_t &bytes_read) -> bool; + +[[nodiscard]] inline auto +read_encrypted_range(const http_range &range, + const utils::hash::hash_256_t &key, + reader_func_t reader_func, std::uint64_t total_size, + data_buffer &data) -> bool { + return read_encrypted_range(range, key, false, reader_func, total_size, data); +} + +[[nodiscard]] inline auto read_encrypted_range( + const http_range &range, const utils::hash::hash_256_t &key, + reader_func_t reader_func, std::uint64_t total_size, unsigned char *data, + std::size_t size, std::size_t &bytes_read) -> bool { + return read_encrypted_range(range, key, false, reader_func, total_size, data, + size, bytes_read); +} + +template +auto create_key_argon2id(string_t password, kdf_config &cfg, + utils::hash::hash_256_t &key) -> bool { + cfg.seal(); + + return recreate_key_argon2id(password, cfg, key); +} + +template +auto recreate_key_argon2id(string_t password, const kdf_config &cfg, + utils::hash::hash_256_t &key) -> bool { + return crypto_pwhash( + reinterpret_cast(key.data()), key.size(), + reinterpret_cast(password.data()), + password.size() * sizeof(typename string_t::value_type), + cfg.salt.data(), get_opslimit(cfg.opslimit), + get_memlimit(cfg.memlimit), crypto_pwhash_ALG_ARGON2ID13) == 0; +} #endif // defined(PROJECT_ENABLE_BOOST) template @@ -218,7 +500,223 @@ inline auto generate_key( return hasher(reinterpret_cast(password.data()), password.size() * sizeof(wchar_t)); } + +#if defined(PROJECT_ENABLE_BOOST) +template +inline auto generate_key_impl(string_t password, kdf_config &cfg) -> hash_t { + MONITARR_USES_FUNCTION_NAME(); + + switch (cfg.version) { + case kdf_version::v1: + switch (cfg.kdf) { + case kdf_type::argon2id: { + hash_t key{}; + if (not create_key_argon2id(password, cfg, key)) { + throw utils::error::create_exception( + function_name, { + "failed to generate argon2id key", + }); + } + + return key; + } + + default: + throw utils::error::create_exception( + function_name, { + "unsupported kdf type", + std::to_string(static_cast(cfg.kdf)), + }); + } + + default: + throw utils::error::create_exception( + function_name, + { + "unsupported kdf version", + std::to_string(static_cast(cfg.version)), + }); + } +} + +template +inline auto recreate_key_impl(string_t password, const kdf_config &cfg) + -> hash_t { + MONITARR_USES_FUNCTION_NAME(); + + switch (cfg.version) { + case kdf_version::v1: + switch (cfg.kdf) { + case kdf_type::argon2id: { + hash_t key{}; + if (not recreate_key_argon2id(password, cfg, key)) { + throw utils::error::create_exception( + function_name, { + "failed to generate argon2id key", + }); + } + + return key; + } + + default: + throw utils::error::create_exception( + function_name, { + "unsupported kdf type", + std::to_string(static_cast(cfg.kdf)), + }); + } + + default: + throw utils::error::create_exception( + function_name, + { + "unsupported kdf version", + std::to_string(static_cast(cfg.version)), + }); + } +} + +template +inline auto generate_key(std::string_view password, kdf_config &cfg) -> hash_t { + return generate_key_impl(password, cfg); +} + +template +inline auto generate_key(std::wstring_view password, kdf_config &cfg) + -> hash_t { + return generate_key_impl(password, cfg); +} + +template +inline auto recreate_key(std::string_view password, const kdf_config &cfg) + -> hash_t { + return recreate_key_impl(password, cfg); +} + +template +inline auto recreate_key(std::wstring_view password, const kdf_config &cfg) + -> hash_t { + return recreate_key_impl(password, cfg); +} + +template +inline auto detect_and_recreate_key(string_t password, data_cspan header, + hash_t &key, std::optional &cfg) + -> bool { + if (header.size() >= kdf_config::size()) { + kdf_config tmp{}; + if (kdf_config::from_header(header.first(kdf_config::size()), tmp)) { + cfg = tmp; + key = recreate_key(password, *cfg); + return true; + } + } + + key = generate_key(password); + return false; +} + +template +inline auto detect_and_recreate_key(std::string_view password, + data_cspan header, hash_t &key, + std::optional &cfg) -> bool { + return detect_and_recreate_key(password, header, + key, cfg); +} + +template +inline auto detect_and_recreate_key(std::wstring_view password, + data_cspan header, hash_t &key, + std::optional &cfg) -> bool { + return detect_and_recreate_key(password, header, + key, cfg); +} + +#endif // defined(PROJECT_ENABLE_BOOST) } // namespace monitarr::utils::encryption +#if defined(PROJECT_ENABLE_BOOST) && defined(PROJECT_ENABLE_JSON) +NLOHMANN_JSON_NAMESPACE_BEGIN + +namespace kdf { +inline constexpr std::string_view JSON_CHECKSUM{"checksum"}; +inline constexpr std::string_view JSON_KDF{"kdf"}; +inline constexpr std::string_view JSON_MEMLIMIT{"memlimit"}; +inline constexpr std::string_view JSON_OPSLIMIT{"opslimit"}; +inline constexpr std::string_view JSON_SALT{"salt"}; +inline constexpr std::string_view JSON_UNIQUE_ID{"unique_id"}; +inline constexpr std::string_view JSON_VERSION{"version"}; +} // namespace kdf + +template <> +struct adl_serializer { + static void + to_json(json &data, + const monitarr::utils::encryption::kdf_config::salt_t &value) { + data = monitarr::utils::collection::to_hex_string(value); + } + + static void + from_json(const json &data, + monitarr::utils::encryption::kdf_config::salt_t &value) { + MONITARR_USES_FUNCTION_NAME(); + + monitarr::data_buffer buffer{}; + if (not monitarr::utils::collection::from_hex_string( + data.get(), buffer)) { + throw monitarr::utils::error::create_exception( + function_name, { + "failed to convert hex string to salt", + data.get(), + }); + } + + if (buffer.size() != value.size()) { + throw monitarr::utils::error::create_exception( + function_name, { + "unexpected length for salt after hex conversion", + "expected", + std::to_string(value.size()), + "actual", + std::to_string(buffer.size()), + }); + } + + std::copy_n(buffer.begin(), value.size(), value.begin()); + } +}; + +template <> struct adl_serializer { + static void to_json(json &data, + const monitarr::utils::encryption::kdf_config &value) { + data[kdf::JSON_CHECKSUM] = value.checksum; + data[kdf::JSON_KDF] = value.kdf; + data[kdf::JSON_MEMLIMIT] = value.memlimit; + data[kdf::JSON_OPSLIMIT] = value.opslimit; + data[kdf::JSON_SALT] = value.salt; + data[kdf::JSON_UNIQUE_ID] = value.unique_id; + data[kdf::JSON_VERSION] = value.version; + } + + static void from_json(const json &data, + monitarr::utils::encryption::kdf_config &value) { + data.at(kdf::JSON_CHECKSUM).get_to(value.checksum); + data.at(kdf::JSON_KDF) + .get_to(value.kdf); + data.at(kdf::JSON_MEMLIMIT) + .get_to(value.memlimit); + data.at(kdf::JSON_OPSLIMIT) + .get_to(value.opslimit); + data.at(kdf::JSON_SALT) + .get_to(value.salt); + data.at(kdf::JSON_UNIQUE_ID).get_to(value.unique_id); + data.at(kdf::JSON_VERSION) + .get_to(value.version); + } +}; +NLOHMANN_JSON_NAMESPACE_END +#endif // defined(PROJECT_ENABLE_BOOST) && defined(PROJECT_ENABLE_JSON) + #endif // defined(PROJECT_ENABLE_LIBSODIUM) #endif // MONITARR_INCLUDE_UTILS_ENCRYPTION_HPP_ diff --git a/support/include/utils/file.hpp b/support/include/utils/file.hpp index a29a059..938eb06 100644 --- a/support/include/utils/file.hpp +++ b/support/include/utils/file.hpp @@ -30,7 +30,7 @@ #include "utils/types/file/i_file.hpp" #include "utils/types/file/i_fs_item.hpp" -namespace repertory::utils::directory { +namespace monitarr::utils::directory { [[nodiscard]] auto temp() -> std::string; } @@ -41,13 +41,13 @@ namespace monitarr::utils::file { [[nodiscard]] auto create_temp_name(std::string_view file_part) -> std::string; // INFO: has test -[[nodiscard]] auto -create_temp_name(std::wstring_view file_part) -> std::wstring; +[[nodiscard]] auto create_temp_name(std::wstring_view file_part) + -> std::wstring; // INFO: has test [[nodiscard]] inline auto -directory_exists_in_path(std::string_view path, - std::string_view sub_directory) -> bool; +directory_exists_in_path(std::string_view path, std::string_view sub_directory) + -> bool; // INFO: has test [[nodiscard]] inline auto @@ -55,45 +55,46 @@ directory_exists_in_path(std::wstring_view path, std::wstring_view sub_directory) -> bool; // INFO: has test -[[nodiscard]] inline auto -file_exists_in_path(std::string_view path, std::string_view file_name) -> bool; +[[nodiscard]] inline auto file_exists_in_path(std::string_view path, + std::string_view file_name) + -> bool; // INFO: has test -[[nodiscard]] inline auto -file_exists_in_path(std::wstring_view path, - std::wstring_view file_name) -> bool; +[[nodiscard]] inline auto file_exists_in_path(std::wstring_view path, + std::wstring_view file_name) + -> bool; // INFO: has test -[[nodiscard]] auto -get_free_drive_space(std::string_view path) -> std::optional; +[[nodiscard]] auto get_free_drive_space(std::string_view path) + -> std::optional; // INFO: has test -[[nodiscard]] auto -get_free_drive_space(std::wstring_view path) -> std::optional; +[[nodiscard]] auto get_free_drive_space(std::wstring_view path) + -> std::optional; // INFO: has test -[[nodiscard]] auto get_time(std::string_view path, - time_type type) -> std::optional; +[[nodiscard]] auto get_time(std::string_view path, time_type type) + -> std::optional; // INFO: has test -[[nodiscard]] auto get_time(std::wstring_view path, - time_type type) -> std::optional; +[[nodiscard]] auto get_time(std::wstring_view path, time_type type) + -> std::optional; // INFO: has test -[[nodiscard]] auto -get_times(std::string_view path) -> std::optional; +[[nodiscard]] auto get_times(std::string_view path) + -> std::optional; // INFO: has test -[[nodiscard]] auto -get_times(std::wstring_view path) -> std::optional; +[[nodiscard]] auto get_times(std::wstring_view path) + -> std::optional; // INFO: has test -[[nodiscard]] auto -get_total_drive_space(std::string_view path) -> std::optional; +[[nodiscard]] auto get_total_drive_space(std::string_view path) + -> std::optional; // INFO: has test -[[nodiscard]] auto -get_total_drive_space(std::wstring_view path) -> std::optional; +[[nodiscard]] auto get_total_drive_space(std::wstring_view path) + -> std::optional; #if defined(PROJECT_ENABLE_LIBDSM) [[nodiscard]] auto @@ -101,20 +102,20 @@ smb_create_and_validate_relative_path(std::string_view smb_path, std::string_view rel_path) -> std::string; // INFO: has test -[[nodiscard]] auto -smb_create_relative_path(std::string_view smb_path) -> std::string; +[[nodiscard]] auto smb_create_relative_path(std::string_view smb_path) + -> std::string; // INFO: has test -[[nodiscard]] auto -smb_create_search_path(std::string_view smb_path) -> std::string; +[[nodiscard]] auto smb_create_search_path(std::string_view smb_path) + -> std::string; // INFO: has test -[[nodiscard]] auto -smb_create_smb_path(std::string_view smb_path, - std::string_view rel_path) -> std::string; +[[nodiscard]] auto smb_create_smb_path(std::string_view smb_path, + std::string_view rel_path) + -> std::string; -[[nodiscard]] auto -smb_get_parent_path(std::string_view smb_path) -> std::string; +[[nodiscard]] auto smb_get_parent_path(std::string_view smb_path) + -> std::string; [[nodiscard]] auto smb_get_root_path(std::string_view smb_path) -> std::string; @@ -143,27 +144,30 @@ read_json_file(std::string_view path, nlohmann::json &data, std::optional password = std::nullopt) -> bool; // INFO: has test -[[nodiscard]] auto read_json_file( - std::wstring_view path, nlohmann::json &data, - std::optional password = std::nullopt) -> bool; +[[nodiscard]] auto +read_json_file(std::wstring_view path, nlohmann::json &data, + std::optional password = std::nullopt) + -> bool; // INFO: has test -[[nodiscard]] auto write_json_file( - std::string_view path, const nlohmann::json &data, - std::optional password = std::nullopt) -> bool; +[[nodiscard]] auto +write_json_file(std::string_view path, const nlohmann::json &data, + std::optional password = std::nullopt) + -> bool; // INFO: has test -[[nodiscard]] auto write_json_file( - std::wstring_view path, const nlohmann::json &data, - std::optional password = std::nullopt) -> bool; +[[nodiscard]] auto +write_json_file(std::wstring_view path, const nlohmann::json &data, + std::optional password = std::nullopt) + -> bool; #else // !defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST) // INFO: has test -[[nodiscard]] auto read_json_file(std::string_view path, - nlohmann::json &data) -> bool; +[[nodiscard]] auto read_json_file(std::string_view path, nlohmann::json &data) + -> bool; // INFO: has test -[[nodiscard]] auto read_json_file(std::wstring_view path, - nlohmann::json &data) -> bool; +[[nodiscard]] auto read_json_file(std::wstring_view path, nlohmann::json &data) + -> bool; // INFO: has test [[nodiscard]] auto write_json_file(std::string_view path, diff --git a/support/include/utils/file_directory.hpp b/support/include/utils/file_directory.hpp index 95c3b88..f7312aa 100644 --- a/support/include/utils/file_directory.hpp +++ b/support/include/utils/file_directory.hpp @@ -49,28 +49,28 @@ private: stop_type *stop_requested_{nullptr}; public: - [[nodiscard]] auto copy_to(std::string_view new_path, - bool overwrite) const -> bool override; + [[nodiscard]] auto copy_to(std::string_view new_path, bool overwrite) const + -> bool override; - [[nodiscard]] auto - count(bool recursive = false) const -> std::uint64_t override; + [[nodiscard]] auto count(bool recursive = false) const + -> std::uint64_t override; - [[nodiscard]] auto - create_directory(std::string_view path = "") const -> fs_directory_t override; + [[nodiscard]] auto create_directory(std::string_view path = "") const + -> fs_directory_t override; [[nodiscard]] auto create_file(std::string_view file_name, bool read_only) const -> fs_file_t override; [[nodiscard]] auto exists() const -> bool override; - [[nodiscard]] auto - get_directory(std::string_view path) const -> fs_directory_t override; + [[nodiscard]] auto get_directory(std::string_view path) const + -> fs_directory_t override; - [[nodiscard]] auto - get_directories() const -> std::vector override; + [[nodiscard]] auto get_directories() const + -> std::vector override; - [[nodiscard]] auto - get_file(std::string_view path) const -> fs_file_t override; + [[nodiscard]] auto get_file(std::string_view path) const + -> fs_file_t override; [[nodiscard]] auto get_files() const -> std::vector override; @@ -88,8 +88,8 @@ public: [[nodiscard]] auto remove_recursively() -> bool override; - [[nodiscard]] auto - size(bool recursive = false) const -> std::uint64_t override; + [[nodiscard]] auto size(bool recursive = false) const + -> std::uint64_t override; public: auto operator=(const directory &) noexcept -> directory & = delete; diff --git a/support/include/utils/hash.hpp b/support/include/utils/hash.hpp index 7f02e8c..0dac225 100644 --- a/support/include/utils/hash.hpp +++ b/support/include/utils/hash.hpp @@ -27,32 +27,55 @@ #include "utils/error.hpp" -namespace monitarr::utils::encryption { +namespace monitarr::utils::hash { +using hash_32_t = std::array; +using hash_64_t = std::array; +using hash_128_t = std::array; using hash_256_t = std::array; using hash_384_t = std::array; using hash_512_t = std::array; +[[nodiscard]] auto create_hash_blake2b_32(std::string_view data) -> hash_32_t; + +[[nodiscard]] auto create_hash_blake2b_32(std::wstring_view data) -> hash_32_t; + +[[nodiscard]] auto create_hash_blake2b_32(const data_buffer &data) -> hash_32_t; + +[[nodiscard]] auto create_hash_blake2b_64(std::string_view data) -> hash_64_t; + +[[nodiscard]] auto create_hash_blake2b_64(std::wstring_view data) -> hash_64_t; + +[[nodiscard]] auto create_hash_blake2b_64(const data_buffer &data) -> hash_64_t; + +[[nodiscard]] auto create_hash_blake2b_128(std::string_view data) -> hash_128_t; + +[[nodiscard]] auto create_hash_blake2b_128(std::wstring_view data) + -> hash_128_t; + +[[nodiscard]] auto create_hash_blake2b_128(const data_buffer &data) + -> hash_128_t; + [[nodiscard]] auto create_hash_blake2b_256(std::string_view data) -> hash_256_t; -[[nodiscard]] auto -create_hash_blake2b_256(std::wstring_view data) -> hash_256_t; +[[nodiscard]] auto create_hash_blake2b_256(std::wstring_view data) + -> hash_256_t; -[[nodiscard]] auto -create_hash_blake2b_256(const data_buffer &data) -> hash_256_t; +[[nodiscard]] auto create_hash_blake2b_256(const data_buffer &data) + -> hash_256_t; [[nodiscard]] auto create_hash_blake2b_384(std::string_view data) -> hash_384_t; -[[nodiscard]] auto -create_hash_blake2b_384(std::wstring_view data) -> hash_384_t; +[[nodiscard]] auto create_hash_blake2b_384(std::wstring_view data) + -> hash_384_t; -[[nodiscard]] auto -create_hash_blake2b_384(const data_buffer &data) -> hash_384_t; +[[nodiscard]] auto create_hash_blake2b_384(const data_buffer &data) + -> hash_384_t; -[[nodiscard]] auto -create_hash_blake2b_512(std::wstring_view data) -> hash_512_t; +[[nodiscard]] auto create_hash_blake2b_512(std::wstring_view data) + -> hash_512_t; -[[nodiscard]] auto -create_hash_blake2b_512(const data_buffer &data) -> hash_512_t; +[[nodiscard]] auto create_hash_blake2b_512(const data_buffer &data) + -> hash_512_t; [[nodiscard]] auto create_hash_blake2b_512(std::string_view data) -> hash_512_t; @@ -83,8 +106,8 @@ template std::function &; template -auto create_hash_blake2b_t(const unsigned char *data, - std::size_t data_size) -> hash_t { +auto create_hash_blake2b_t(const unsigned char *data, std::size_t data_size) + -> hash_t { MONITARR_USES_FUNCTION_NAME(); hash_t hash{}; @@ -123,6 +146,27 @@ auto create_hash_blake2b_t(const unsigned char *data, return hash; } +inline const std::function + blake2b_32_hasher = + [](const unsigned char *data, std::size_t data_size) -> hash_32_t { + return create_hash_blake2b_t(data, data_size); +}; + +inline const std::function + blake2b_64_hasher = + [](const unsigned char *data, std::size_t data_size) -> hash_64_t { + return create_hash_blake2b_t(data, data_size); +}; + +inline const std::function + blake2b_128_hasher = + [](const unsigned char *data, std::size_t data_size) -> hash_128_t { + return create_hash_blake2b_t(data, data_size); +}; + inline const std::function blake2b_256_hasher = @@ -158,6 +202,24 @@ inline const std::function +[[nodiscard]] inline auto default_create_hash() -> const + std::function & { + return blake2b_32_hasher; +} + +template <> +[[nodiscard]] inline auto default_create_hash() -> const + std::function & { + return blake2b_64_hasher; +} + +template <> +[[nodiscard]] inline auto default_create_hash() -> const + std::function & { + return blake2b_128_hasher; +} + template <> [[nodiscard]] inline auto default_create_hash() -> const std::function & { @@ -175,7 +237,7 @@ template <> std::function & { return blake2b_512_hasher; } -} // namespace monitarr::utils::encryption +} // namespace monitarr::utils::hash #endif // defined(PROJECT_ENABLE_LIBSODIUM) #endif // MONITARR_INCLUDE_UTILS_HASH_HPP_ diff --git a/support/include/utils/timeout.hpp b/support/include/utils/timeout.hpp new file mode 100644 index 0000000..39daffc --- /dev/null +++ b/support/include/utils/timeout.hpp @@ -0,0 +1,59 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef MONITARR_INCLUDE_UTILS_TIMEOUT_HPP_ +#define MONITARR_INCLUDE_UTILS_TIMEOUT_HPP_ + +#include "utils/config.hpp" + +namespace monitarr::utils { +class timeout final { +public: + using callback_t = std::function; + +public: + timeout(const timeout &) noexcept = delete; + timeout(timeout &&) noexcept = delete; + auto operator=(const timeout &) noexcept -> timeout & = delete; + auto operator=(timeout &&) noexcept -> timeout & = delete; + +public: + timeout(callback_t timeout_callback, + std::chrono::system_clock::duration duration); + + ~timeout(); + +private: + std::chrono::system_clock::duration duration_; + callback_t timeout_callback_; + std::atomic timeout_killed_{false}; + std::unique_ptr timeout_thread_{nullptr}; + std::mutex timeout_mutex_; + std::condition_variable timeout_notify_; + +public: + void disable(); + + void reset(); +}; +} // namespace monitarr::utils + +#endif // MONITARR_INCLUDE_UTILS_TIMEOUT_HPP_ diff --git a/support/include/utils/ttl_cache.hpp b/support/include/utils/ttl_cache.hpp new file mode 100644 index 0000000..0866f6c --- /dev/null +++ b/support/include/utils/ttl_cache.hpp @@ -0,0 +1,100 @@ +#ifndef MONITARR_INCLUDE_UTILS_TTL_CACHE_HPP_ +#define MONITARR_INCLUDE_UTILS_TTL_CACHE_HPP_ + +#include "utils/config.hpp" + +namespace monitarr::utils { +template class atomic_t = std::atomic> +class ttl_cache final { +public: + using clock = std::chrono::steady_clock; + using duration = std::chrono::milliseconds; + using entry_t = atomic_t; + using entry_ptr_t = std::shared_ptr; + + static constexpr auto default_expiration{duration(60000U)}; + +private: + struct entry final { + entry_ptr_t data; + clock::time_point expires_at; + }; + +public: + ttl_cache(duration ttl = default_expiration) : ttl_{ttl} {} + +private: + duration ttl_; + +private: + mutable std::mutex mutex_; + std::unordered_map entries_; + +public: + void clear() { + mutex_lock lock(mutex_); + entries_.clear(); + } + + void erase(const std::string &api_path) { + mutex_lock lock(mutex_); + entries_.erase(api_path); + } + + [[nodiscard]] auto contains(const std::string &api_path) -> bool { + mutex_lock lock(mutex_); + return entries_.contains(api_path); + } + + [[nodiscard]] auto get(const std::string &api_path) -> entry_ptr_t { + mutex_lock lock(mutex_); + auto iter = entries_.find(api_path); + if (iter == entries_.end()) { + return nullptr; + } + + iter->second.expires_at = clock::now() + ttl_; + return iter->second.data; + } + + void purge_expired() { + mutex_lock lock(mutex_); + auto now = clock::now(); + for (auto iter = entries_.begin(); iter != entries_.end();) { + if (iter->second.expires_at <= now) { + iter = entries_.erase(iter); + continue; + } + + ++iter; + } + } + + [[nodiscard]] auto get_ttl() const -> duration { + mutex_lock lock(mutex_); + return ttl_; + } + + void set(const std::string &api_path, const data_t &data) { + mutex_lock lock(mutex_); + if (entries_.contains(api_path)) { + auto &entry = entries_.at(api_path); + entry.data->store(data); + entry.expires_at = clock::now() + ttl_; + return; + } + + entries_.emplace(api_path, entry{ + .data = std::make_shared(data), + .expires_at = clock::now() + ttl_, + }); + } + + void set_ttl(duration ttl) { + mutex_lock lock(mutex_); + ttl_ = ttl; + } +}; +} // namespace monitarr::utils + +#endif // MONITARR_INCLUDE_UTILS_TTL_CACHE_HPP_ diff --git a/support/include/utils/types/file/i_fs_item.hpp b/support/include/utils/types/file/i_fs_item.hpp index 9eee5df..309ac08 100644 --- a/support/include/utils/types/file/i_fs_item.hpp +++ b/support/include/utils/types/file/i_fs_item.hpp @@ -30,6 +30,7 @@ namespace monitarr::utils::file { enum class time_type { accessed, + changed, created, modified, written, @@ -37,6 +38,7 @@ enum class time_type { struct file_times final { std::uint64_t accessed{}; + std::uint64_t changed{}; std::uint64_t created{}; std::uint64_t modified{}; std::uint64_t written{}; @@ -47,6 +49,8 @@ struct file_times final { switch (type) { case time_type::accessed: return accessed; + case time_type::changed: + return changed; case time_type::created: return created; case time_type::modified: @@ -70,8 +74,8 @@ struct i_fs_item { [[nodiscard]] virtual auto copy_to(std::string_view to_path, bool overwrite) const -> bool = 0; - [[nodiscard]] virtual auto copy_to(std::wstring_view new_path, - bool overwrite) -> bool { + [[nodiscard]] virtual auto copy_to(std::wstring_view new_path, bool overwrite) + -> bool { return copy_to(utils::string::to_utf8(new_path), overwrite); } @@ -79,8 +83,8 @@ struct i_fs_item { [[nodiscard]] virtual auto get_path() const -> std::string = 0; - [[nodiscard]] virtual auto - get_time(time_type type) const -> std::optional; + [[nodiscard]] virtual auto get_time(time_type type) const + -> std::optional; [[nodiscard]] virtual auto is_directory_item() const -> bool = 0; diff --git a/support/include/utils/unix.hpp b/support/include/utils/unix.hpp index c625175..cdcc8a5 100644 --- a/support/include/utils/unix.hpp +++ b/support/include/utils/unix.hpp @@ -27,6 +27,40 @@ #include "utils/config.hpp" namespace monitarr::utils { +#if defined(__linux__) +struct autostart_cfg final { + std::string app_name; + std::optional comment; + bool enabled{true}; + std::vector exec_args; + std::string exec_path; + std::optional icon_path; + std::vector only_show_in; + bool terminal{false}; +}; +#endif // defined(__linux__) + +#if defined(__APPLE__) +enum class launchctl_type : std::uint8_t { + bootout, + bootstrap, + kickstart, +}; + +#if defined(PROJECT_ENABLE_PUGIXML) +struct plist_cfg final { + std::vector args; + bool keep_alive{false}; + std::string label; + std::string plist_path; + bool run_at_load{false}; + std::string stderr_log{"/tmp/stderr.log"}; + std::string stdout_log{"/tmp/stdout.log"}; + std::string working_dir{"/tmp"}; +}; +#endif // defined(PROJECT_ENABLE_PUGIXML) +#endif // defined(__APPLE__) + using passwd_callback_t = std::function; #if defined(__APPLE__) @@ -37,6 +71,12 @@ template [[nodiscard]] auto convert_to_uint64(const pthread_t &thread) -> std::uint64_t; #endif // defined(__APPLE__) +#if defined(__linux__) +[[nodiscard]] auto create_autostart_entry(const autostart_cfg &cfg, + bool overwrite_existing = true) + -> bool; +#endif // defined(__linux__) + [[nodiscard]] auto get_last_error_code() -> int; [[nodiscard]] auto get_thread_id() -> std::uint64_t; @@ -48,9 +88,30 @@ void set_last_error_code(int error_code); [[nodiscard]] auto use_getpwuid(uid_t uid, passwd_callback_t callback) -> utils::result; +#if defined(__linux__) +[[nodiscard]] auto remove_autostart_entry(std::string_view name) -> bool; +#endif // defined(__linux__) + +#if defined(__APPLE__) +#if defined(PROJECT_ENABLE_PUGIXML) +[[nodiscard]] auto generate_launchd_plist(const plist_cfg &cfg, + bool overwrite_existing = true) + -> bool; +#endif // defined(PROJECT_ENABLE_PUGIXML) + +#if defined(PROJECT_ENABLE_SPDLOG) || defined(PROJECT_ENABLE_FMT) +[[nodiscard]] auto launchctl_command(std::string_view label, + launchctl_type type) -> int; + +[[nodiscard]] auto remove_launchd_plist(std::string_view plist_path, + std::string_view label, + bool should_bootout) -> bool; +#endif // defined(PROJECT_ENABLE_SPDLOG) || defined(PROJECT_ENABLE_FMT) +#endif // defined(__APPLE__) + // template implementations #if defined(__APPLE__) -template +template [[nodiscard]] auto convert_to_uint64(const thread_t *thread_ptr) -> std::uint64_t { return static_cast( diff --git a/support/include/utils/windows.hpp b/support/include/utils/windows.hpp index 49c1d92..e2c52fe 100644 --- a/support/include/utils/windows.hpp +++ b/support/include/utils/windows.hpp @@ -30,17 +30,42 @@ void create_console(); void free_console(); +[[nodiscard]] auto get_available_drive_letter(char first = 'a') + -> std::optional; + +[[nodiscard]] auto get_available_drive_letters(char first = 'a') + -> std::vector; + [[nodiscard]] auto get_local_app_data_directory() -> const std::string &; [[nodiscard]] auto get_last_error_code() -> DWORD; +[[nodiscard]] auto get_startup_folder() -> std::wstring; + [[nodiscard]] auto get_thread_id() -> std::uint64_t; [[nodiscard]] auto is_process_elevated() -> bool; [[nodiscard]] auto run_process_elevated(std::vector args) -> int; -void set_last_error_code(DWORD errorCode); +struct shortcut_cfg final { + std::wstring arguments; + std::wstring exe_path; + std::wstring icon_path; + std::wstring location{get_startup_folder()}; + std::wstring shortcut_name; + std::wstring working_directory; +}; + +[[nodiscard]] +auto create_shortcut(const shortcut_cfg &cfg, bool overwrite_existing = true) + -> bool; + +[[nodiscard]] auto +remove_shortcut(std::wstring shortcut_name, + const std::wstring &location = get_startup_folder()) -> bool; + +void set_last_error_code(DWORD error_code); } // namespace monitarr::utils #endif // defined(_WIN32) diff --git a/support/src/utils/encrypting_reader.cpp b/support/src/utils/encrypting_reader.cpp index b9b70b3..2601698 100644 --- a/support/src/utils/encrypting_reader.cpp +++ b/support/src/utils/encrypting_reader.cpp @@ -23,8 +23,10 @@ #include "utils/encrypting_reader.hpp" +#include "utils/base64.hpp" #include "utils/collection.hpp" #include "utils/common.hpp" +#include "utils/config.hpp" #include "utils/encryption.hpp" #include "utils/error.hpp" #include "utils/file.hpp" @@ -119,7 +121,7 @@ protected: reader_.set_read_position(reinterpret_cast(gptr())); char c{}; - const auto res = encrypting_reader::reader_function(&c, 1U, 1U, &reader_); + auto res = encrypting_reader::reader_function(&c, 1U, 1U, &reader_); if (res != 1) { return traits_type::eof(); } @@ -180,171 +182,218 @@ encrypting_reader::encrypting_reader( std::string_view file_name, std::string_view source_path, stop_type_callback stop_requested_cb, std::string_view token, std::optional relative_parent_path, std::size_t error_return) - : key_(utils::encryption::generate_key( - token)), + : keys_(utils::encryption::generate_key(token), + utils::encryption::generate_key(token)), stop_requested_cb_(std::move(stop_requested_cb)), error_return_(error_return), source_file_(utils::file::file::open_or_create_file(source_path, true)) { - MONITARR_USES_FUNCTION_NAME(); - - if (not*source_file_) { - throw utils::error::create_exception(function_name, { - "file open failed", - source_path, - }); - } - - data_buffer result; - utils::encryption::encrypt_data( - key_, reinterpret_cast(file_name.data()), - file_name.size(), result); - encrypted_file_name_ = utils::collection::to_hex_string(result); - - if (relative_parent_path.has_value()) { - for (auto &&part : - utils::string::split(relative_parent_path.value(), - utils::path::directory_seperator, false)) { - utils::encryption::encrypt_data( - key_, reinterpret_cast(part.c_str()), - strnlen(part.c_str(), part.size()), result); - encrypted_file_path_ += '/' + utils::collection::to_hex_string(result); - } - encrypted_file_path_ += '/' + encrypted_file_name_; - } - - auto opt_size = source_file_->size(); - if (not opt_size.has_value()) { - throw utils::error::create_exception(function_name, - { - "failed to get file size", - source_file_->get_path(), - }); - } - auto file_size = opt_size.value(); - - const auto total_chunks = utils::divide_with_ceiling( - file_size, static_cast(data_chunk_size_)); - total_size_ = file_size + (total_chunks * encryption_header_size); - last_data_chunk_ = total_chunks - 1U; - last_data_chunk_size_ = (file_size <= data_chunk_size_) ? file_size - : (file_size % data_chunk_size_) == 0U - ? data_chunk_size_ - : file_size % data_chunk_size_; - iv_list_.resize(total_chunks); - for (auto &iv : iv_list_) { - randombytes_buf(iv.data(), iv.size()); - } + common_initialize(true); + create_encrypted_paths(file_name, relative_parent_path); } -encrypting_reader::encrypting_reader(std::string_view encrypted_file_path, +encrypting_reader::encrypting_reader(stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, std::string_view source_path, - stop_type_callback stop_requested_cb, std::string_view token, std::size_t error_return) - : key_(utils::encryption::generate_key( - token)), + : keys_(utils::encryption::generate_key(token), + utils::encryption::generate_key(token)), stop_requested_cb_(std::move(stop_requested_cb)), error_return_(error_return), - source_file_(utils::file::file::open_or_create_file(source_path, true)) { - MONITARR_USES_FUNCTION_NAME(); - - if (not*source_file_) { - throw utils::error::create_exception(function_name, { - "file open failed", - source_path, - }); - } - - encrypted_file_path_ = encrypted_file_path; - encrypted_file_name_ = utils::path::strip_to_file_name(encrypted_file_path_); - - auto opt_size = source_file_->size(); - if (not opt_size.has_value()) { - throw utils::error::create_exception(function_name, - { - "failed to get file size", - source_file_->get_path(), - }); - } - auto file_size = opt_size.value(); - - const auto total_chunks = utils::divide_with_ceiling( - file_size, static_cast(data_chunk_size_)); - total_size_ = file_size + (total_chunks * encryption_header_size); - last_data_chunk_ = total_chunks - 1U; - last_data_chunk_size_ = (file_size <= data_chunk_size_) ? file_size - : (file_size % data_chunk_size_) == 0U - ? data_chunk_size_ - : file_size % data_chunk_size_; - iv_list_.resize(total_chunks); - for (auto &iv : iv_list_) { - randombytes_buf(iv.data(), iv.size()); - } + source_file_(utils::file::file::open_or_create_file(source_path, true)), + encrypted_file_name_( + utils::path::strip_to_file_name(std::string{encrypted_file_path})), + encrypted_file_path_(encrypted_file_path) { + common_initialize(true); } encrypting_reader::encrypting_reader( - std::string_view encrypted_file_path, std::string_view source_path, - stop_type_callback stop_requested_cb, std::string_view token, + stop_type_callback stop_requested_cb, std::string_view encrypted_file_path, + std::string_view source_path, std::string_view token, std::vector< std::array> iv_list, std::size_t error_return) - : key_(utils::encryption::generate_key( - token)), + : keys_(utils::encryption::generate_key(token), + utils::encryption::generate_key(token)), stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)), + encrypted_file_name_( + utils::path::strip_to_file_name(std::string{encrypted_file_path})), + encrypted_file_path_(encrypted_file_path), + iv_list_(std::move(iv_list)) { + common_initialize(false); +} + +encrypting_reader::encrypting_reader( + std::string_view file_name, std::string_view source_path, + stop_type_callback stop_requested_cb, std::string_view token, + kdf_config cfg, std::optional relative_parent_path, + std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), error_return_(error_return), source_file_(utils::file::file::open_or_create_file(source_path, true)) { - MONITARR_USES_FUNCTION_NAME(); + common_initialize_kdf_keys(token, cfg); + common_initialize(true); + create_encrypted_paths(file_name, relative_parent_path); +} - if (not*source_file_) { - throw utils::error::create_exception(function_name, { - "file open failed", - source_path, - }); - } +encrypting_reader::encrypting_reader(stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, + std::string_view source_path, + std::string_view token, kdf_config cfg, + std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)), + encrypted_file_name_( + utils::path::strip_to_file_name(std::string{encrypted_file_path})), + encrypted_file_path_(encrypted_file_path) { + common_initialize_kdf_keys(token, cfg); + common_initialize(true); +} - encrypted_file_path_ = encrypted_file_path; - encrypted_file_name_ = utils::path::strip_to_file_name(encrypted_file_path_); +encrypting_reader::encrypting_reader( + stop_type_callback stop_requested_cb, std::string_view encrypted_file_path, + std::string_view source_path, std::string_view token, kdf_config cfg, + std::vector< + std::array> + iv_list, + std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)), + encrypted_file_name_( + utils::path::strip_to_file_name(std::string{encrypted_file_path})), + encrypted_file_path_(encrypted_file_path), + iv_list_(std::move(iv_list)) { + common_initialize_kdf_keys(token, cfg); + common_initialize(false); +} - auto opt_size = source_file_->size(); - if (not opt_size.has_value()) { - throw utils::error::create_exception( - function_name, { - "get file size failed", - std::to_string(utils::get_last_error_code()), - source_file_->get_path(), - }); - } - auto file_size{opt_size.value()}; +encrypting_reader::encrypting_reader( + std::string_view file_name, std::string_view source_path, + stop_type_callback stop_requested_cb, + const utils::hash::hash_256_t &master_key, const kdf_config &cfg, + std::optional relative_parent_path, std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)) { + common_initialize_kdf_data(cfg, master_key); + auto [path_key, path_cfg] = cfg.create_subkey( + kdf_context::path, utils::generate_secure_random(), + master_key); + keys_.second = std::move(path_key); + kdf_headers_->second = path_cfg.to_header(); + common_initialize(true); + create_encrypted_paths(file_name, relative_parent_path); +} - const auto total_chunks = utils::divide_with_ceiling( - file_size, static_cast(data_chunk_size_)); - total_size_ = file_size + (total_chunks * encryption_header_size); - last_data_chunk_ = total_chunks - 1U; - last_data_chunk_size_ = (file_size <= data_chunk_size_) ? file_size - : (file_size % data_chunk_size_) == 0U - ? data_chunk_size_ - : file_size % data_chunk_size_; - iv_list_ = std::move(iv_list); +encrypting_reader::encrypting_reader( + std::string_view file_name, std::string_view source_path, + stop_type_callback stop_requested_cb, + const utils::hash::hash_256_t &master_key, + const std::pair &configs, + std::optional relative_parent_path, std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)) { + keys_ = { + configs.first.recreate_subkey(utils::encryption::kdf_context::data, + master_key), + configs.second.recreate_subkey(utils::encryption::kdf_context::path, + master_key), + }; + kdf_headers_ = { + configs.first.to_header(), + configs.second.to_header(), + }; + common_initialize(true); + create_encrypted_paths(file_name, relative_parent_path); +} + +encrypting_reader::encrypting_reader(stop_type_callback stop_requested_cb, + std::string_view encrypted_file_path, + std::string_view source_path, + const utils::hash::hash_256_t &master_key, + const kdf_config &cfg, + std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)), + encrypted_file_name_( + utils::path::strip_to_file_name(std::string{encrypted_file_path})), + encrypted_file_path_(encrypted_file_path) { + common_initialize_kdf_data(cfg, master_key); + common_initialize_kdf_path(master_key); + common_initialize(true); +} + +encrypting_reader::encrypting_reader( + stop_type_callback stop_requested_cb, std::string_view encrypted_file_path, + std::string_view source_path, const utils::hash::hash_256_t &master_key, + const kdf_config &cfg, + std::vector< + std::array> + iv_list, + std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)), + encrypted_file_name_( + utils::path::strip_to_file_name(std::string{encrypted_file_path})), + encrypted_file_path_(encrypted_file_path), + iv_list_(std::move(iv_list)) { + common_initialize_kdf_data(cfg, master_key); + common_initialize_kdf_path(master_key); + common_initialize(false); +} + +encrypting_reader::encrypting_reader( + stop_type_callback stop_requested_cb, std::string_view encrypted_file_path, + std::string_view source_path, const utils::hash::hash_256_t &master_key, + const std::pair &configs, + std::vector< + std::array> + iv_list, + std::size_t error_return) + : stop_requested_cb_(std::move(stop_requested_cb)), + error_return_(error_return), + source_file_(utils::file::file::open_or_create_file(source_path, true)), + encrypted_file_name_( + utils::path::strip_to_file_name(std::string{encrypted_file_path})), + encrypted_file_path_(encrypted_file_path), + iv_list_(std::move(iv_list)) { + keys_.first = configs.first.recreate_subkey( + utils::encryption::kdf_context::data, master_key); + keys_.second = configs.second.recreate_subkey( + utils::encryption::kdf_context::path, master_key); + kdf_headers_ = { + configs.first.to_header(), + configs.second.to_header(), + }; + common_initialize(false); } encrypting_reader::encrypting_reader(const encrypting_reader &reader) - : key_(reader.key_), + : keys_(reader.keys_), stop_requested_cb_(reader.stop_requested_cb_), error_return_(reader.error_return_), source_file_( utils::file::file::open_file(reader.source_file_->get_path(), true)), - chunk_buffers_(reader.chunk_buffers_), encrypted_file_name_(reader.encrypted_file_name_), encrypted_file_path_(reader.encrypted_file_path_), iv_list_(reader.iv_list_), + chunk_buffers_(reader.chunk_buffers_), + kdf_headers_(reader.kdf_headers_), last_data_chunk_(reader.last_data_chunk_), last_data_chunk_size_(reader.last_data_chunk_size_), read_offset_(reader.read_offset_), total_size_(reader.total_size_) { MONITARR_USES_FUNCTION_NAME(); - if (not*source_file_) { + if (not *source_file_) { throw utils::error::create_exception( function_name, { "file open failed", @@ -354,15 +403,21 @@ encrypting_reader::encrypting_reader(const encrypting_reader &reader) } } -auto encrypting_reader::calculate_decrypted_size(std::uint64_t total_size) +auto encrypting_reader::calculate_decrypted_size(std::uint64_t total_size, + bool uses_kdf) -> std::uint64_t { + if (uses_kdf) { + total_size -= kdf_config::size(); + } + return total_size - (utils::divide_with_ceiling( total_size, static_cast( get_encrypted_chunk_size())) * encryption_header_size); } -auto encrypting_reader::calculate_encrypted_size(std::string_view source_path) +auto encrypting_reader::calculate_encrypted_size(std::string_view source_path, + bool uses_kdf) -> std::uint64_t { MONITARR_USES_FUNCTION_NAME(); @@ -375,11 +430,135 @@ auto encrypting_reader::calculate_encrypted_size(std::string_view source_path) source_path, }); } - auto file_size{opt_size.value()}; - const auto total_chunks = utils::divide_with_ceiling( + return calculate_encrypted_size(opt_size.value(), uses_kdf); +} + +auto encrypting_reader::calculate_encrypted_size(std::uint64_t size, + bool uses_kdf) + -> std::uint64_t { + auto total_chunks = utils::divide_with_ceiling( + size, static_cast(data_chunk_size_)); + return size + (total_chunks * encryption_header_size) + + (uses_kdf ? kdf_config::size() : 0U); +} + +void encrypting_reader::common_initialize(bool procces_iv_list) { + MONITARR_USES_FUNCTION_NAME(); + + if (not *source_file_) { + throw utils::error::create_exception(function_name, + { + "file open failed", + source_file_->get_path(), + }); + } + + auto opt_size = source_file_->size(); + if (not opt_size.has_value()) { + throw utils::error::create_exception(function_name, + { + "failed to get file size", + source_file_->get_path(), + }); + } + auto file_size = opt_size.value(); + + auto total_chunks = utils::divide_with_ceiling( file_size, static_cast(data_chunk_size_)); - return file_size + (total_chunks * encryption_header_size); + total_size_ = file_size + (total_chunks * encryption_header_size) + + (kdf_headers_.has_value() ? kdf_headers_->first.size() : 0U); + last_data_chunk_ = total_chunks - 1U; + last_data_chunk_size_ = (file_size <= data_chunk_size_) ? file_size + : (file_size % data_chunk_size_) == 0U + ? data_chunk_size_ + : file_size % data_chunk_size_; + if (not procces_iv_list) { + return; + } + + iv_list_.resize(total_chunks); + for (auto &data : iv_list_) { + randombytes_buf(data.data(), data.size()); + } +} + +void encrypting_reader::common_initialize_kdf_data( + const kdf_config &cfg, const utils::hash::hash_256_t &master_key) { + auto [data_key, data_cfg] = cfg.create_subkey( + kdf_context::data, utils::generate_secure_random(), + master_key); + keys_.first = std::move(data_key); + kdf_headers_ = {data_cfg.to_header(), {}}; +} + +void encrypting_reader::common_initialize_kdf_keys(std::string_view token, + kdf_config &cfg) { + auto key = + utils::encryption::generate_key(token, cfg); + keys_ = {key, key}; + kdf_headers_ = {cfg.to_header(), cfg.to_header()}; +} + +void encrypting_reader::common_initialize_kdf_path( + const utils::hash::hash_256_t &master_key) { + MONITARR_USES_FUNCTION_NAME(); + + auto buffer = macaron::Base64::Decode(encrypted_file_path_); + + kdf_config path_cfg; + if (not kdf_config::from_header(buffer, path_cfg)) { + throw utils::error::create_exception( + function_name, {"failed to create path kdf config from header"}); + } + + utils::hash::hash_256_t path_key; + std::tie(path_key, std::ignore) = + path_cfg.create_subkey(kdf_context::path, path_cfg.unique_id, master_key); + + kdf_headers_->second = path_cfg.to_header(); +} + +void encrypting_reader::create_encrypted_paths( + std::string_view file_name, + std::optional relative_parent_path) { + data_buffer result; + utils::encryption::encrypt_data( + keys_.second, reinterpret_cast(file_name.data()), + file_name.size(), result); + if (kdf_headers_.has_value()) { + result.insert(result.begin(), kdf_headers_->second.begin(), + kdf_headers_->second.end()); + } + + encrypted_file_name_ = + kdf_headers_.has_value() + ? macaron::Base64::EncodeUrlSafe(result.data(), result.size()) + : utils::collection::to_hex_string(result); + + if (not relative_parent_path.has_value()) { + return; + } + + for (const auto &part : + utils::string::split(relative_parent_path.value(), + utils::path::directory_seperator, false)) { + utils::encryption::encrypt_data( + keys_.second, reinterpret_cast(part.c_str()), + strnlen(part.c_str(), part.size()), result); + if (kdf_headers_.has_value()) { + result.insert(result.begin(), kdf_headers_->second.begin(), + kdf_headers_->second.end()); + } + + encrypted_file_path_ += + '/' + + (kdf_headers_.has_value() + ? macaron::Base64::EncodeUrlSafe(result.data(), result.size()) + : utils::collection::to_hex_string(result)); + } + + encrypted_file_path_ += '/' + encrypted_file_name_; } auto encrypting_reader::create_iostream() const @@ -388,24 +567,93 @@ auto encrypting_reader::create_iostream() const std::make_unique(*this)); } +auto encrypting_reader::get_kdf_config_for_data() const + -> std::optional { + MONITARR_USES_FUNCTION_NAME(); + + if (not kdf_headers_.has_value()) { + return std::nullopt; + } + + kdf_config cfg; + if (not kdf_config::from_header(kdf_headers_->first, cfg)) { + throw utils::error::create_exception(function_name, + { + "invalid kdf header", + }); + } + + return cfg; +} + +auto encrypting_reader::get_kdf_config_for_path() const + -> std::optional { + MONITARR_USES_FUNCTION_NAME(); + + if (not kdf_headers_.has_value()) { + return std::nullopt; + } + + kdf_config cfg; + if (not kdf_config::from_header(kdf_headers_->second, cfg)) { + throw utils::error::create_exception(function_name, + { + "invalid kdf header", + }); + } + + return cfg; +} + auto encrypting_reader::reader_function(char *buffer, size_t size, size_t nitems) -> size_t { MONITARR_USES_FUNCTION_NAME(); - const auto read_size = static_cast(std::min( - static_cast(size * nitems), total_size_ - read_offset_)); + auto read_size = + static_cast(size) * static_cast(nitems); + if (read_size == 0U) { + return 0U; + } - auto chunk = read_offset_ / encrypted_chunk_size_; - auto chunk_offset = read_offset_ % encrypted_chunk_size_; + std::span dest(buffer, read_size); + auto read_offset{read_offset_}; std::size_t total_read{}; + auto total_size{total_size_}; - auto ret = false; - if (read_offset_ < total_size_) { + if (kdf_headers_.has_value()) { + auto &hdr = kdf_headers_->first; + total_size -= hdr.size(); + + if (read_offset < hdr.size()) { + auto to_read{ + utils::calculate_read_size(hdr.size(), read_size, read_offset), + }; + read_offset_ += to_read; + + std::memcpy(&dest[total_read], &hdr.at(read_offset), to_read); + if (read_size - to_read == 0) { + return to_read; + } + + read_offset = 0U; + read_size -= to_read; + total_read += to_read; + } else { + read_offset -= hdr.size(); + } + } + + auto chunk = static_cast(read_offset / encrypted_chunk_size_); + auto chunk_offset = + static_cast(read_offset % encrypted_chunk_size_); + auto remain = utils::calculate_read_size(total_size, read_size, read_offset); + + auto ret{false}; + if (read_offset < total_size) { try { ret = true; - auto remain = read_size; while (not get_stop_requested() && ret && (remain != 0U)) { - if (chunk_buffers_.find(chunk) == chunk_buffers_.end()) { + if (not chunk_buffers_.contains(chunk)) { auto &chunk_buffer = chunk_buffers_[chunk]; data_buffer file_data(chunk == last_data_chunk_ ? last_data_chunk_size_ @@ -413,24 +661,26 @@ auto encrypting_reader::reader_function(char *buffer, size_t size, chunk_buffer.resize(file_data.size() + encryption_header_size); std::size_t bytes_read{}; - if ((ret = source_file_->read(file_data, chunk * data_chunk_size_, - &bytes_read))) { - utils::encryption::encrypt_data(iv_list_.at(chunk), key_, file_data, - chunk_buffer); + ret = source_file_->read( + file_data, + static_cast(chunk) * + static_cast(data_chunk_size_), + &bytes_read); + if (ret) { + utils::encryption::encrypt_data(iv_list_.at(chunk), keys_.first, + file_data, chunk_buffer); } - } else if (chunk) { - chunk_buffers_.erase(chunk - 1u); + } else if (chunk != 0U) { + chunk_buffers_.erase(chunk - 1U); } auto &chunk_buffer = chunk_buffers_[chunk]; - const auto to_read = std::min( - static_cast(chunk_buffer.size() - chunk_offset), - remain); - std::memcpy(buffer + total_read, &chunk_buffer[chunk_offset], to_read); + auto to_read = std::min(chunk_buffer.size() - chunk_offset, remain); + std::memcpy(&dest[total_read], &chunk_buffer[chunk_offset], to_read); total_read += to_read; remain -= to_read; - chunk_offset = 0u; - chunk++; + chunk_offset = 0U; + ++chunk; read_offset_ += to_read; } } catch (const std::exception &e) { diff --git a/support/src/utils/encryption.cpp b/support/src/utils/encryption.cpp index bb5f7e4..1b069b9 100644 --- a/support/src/utils/encryption.cpp +++ b/support/src/utils/encryption.cpp @@ -23,11 +23,130 @@ #include "utils/encryption.hpp" +#include "utils/base64.hpp" #include "utils/collection.hpp" +#include "utils/config.hpp" #include "utils/encrypting_reader.hpp" +#include "utils/hash.hpp" #include "utils/path.hpp" +namespace { +constexpr auto resize_by(monitarr::data_span &data, std::size_t /* size */) + -> monitarr::data_span & { + return data; +} + +auto resize_by(monitarr::data_buffer &data, std::size_t size) + -> monitarr::data_buffer & { + data.resize(data.size() + size); + return data; +} +} // namespace + namespace monitarr::utils::encryption { +auto kdf_config::to_header() const -> data_buffer { + kdf_config tmp{*this}; + tmp.checksum = boost::endian::native_to_big(tmp.checksum); + tmp.unique_id = boost::endian::native_to_big(tmp.unique_id); + + data_buffer ret(size()); + std::memcpy(ret.data(), &tmp, ret.size()); + return ret; +} + +auto kdf_config::generate_checksum() const -> std::uint64_t { + MONITARR_USES_FUNCTION_NAME(); + + kdf_config tmp = *this; + tmp.checksum = 0; + + auto hash = utils::hash::create_hash_blake2b_64(tmp.to_header()); + std::uint64_t ret{}; + std::memcpy(&ret, hash.data(), hash.size()); + return ret; +} + +auto kdf_config::from_header(data_cspan data, kdf_config &cfg, + bool ignore_checksum) -> bool { + if (data.size() < kdf_config::size()) { + return false; + } + + std::memcpy(&cfg, data.data(), kdf_config::size()); + + cfg.checksum = boost::endian::big_to_native(cfg.checksum); + cfg.unique_id = boost::endian::big_to_native(cfg.unique_id); + return cfg.version == kdf_version::v1 && cfg.kdf == kdf_type::argon2id && + cfg.memlimit >= memlimit_level::level1 && + cfg.memlimit <= memlimit_level::level4 && + cfg.opslimit >= opslimit_level::level1 && + cfg.opslimit <= opslimit_level::level3 && + (ignore_checksum || cfg.checksum == cfg.generate_checksum()); +} + +void kdf_config::seal() { + randombytes_buf(salt.data(), salt.size()); + checksum = generate_checksum(); +} + +auto decrypt_file_name(std::string_view encryption_token, + std::string &file_name) -> bool { + data_buffer buffer; + if (not utils::collection::from_hex_string(file_name, buffer)) { + return false; + } + + file_name.clear(); + return utils::encryption::decrypt_data(encryption_token, buffer, file_name); +} + +auto decrypt_file_name(std::string_view encryption_token, const kdf_config &cfg, + std::string &file_name) -> bool { + MONITARR_USES_FUNCTION_NAME(); + + try { + auto buffer = macaron::Base64::Decode(file_name); + + file_name.clear(); + return utils::encryption::decrypt_data(encryption_token, cfg, buffer, + file_name); + } catch (const std::exception &e) { + utils::error::handle_exception(function_name, e); + } catch (...) { + utils::error::handle_exception(function_name); + } + + return false; +} + +auto decrypt_file_name(const utils::hash::hash_256_t &master_key, + std::string &file_name) -> bool { + MONITARR_USES_FUNCTION_NAME(); + + try { + auto buffer = macaron::Base64::Decode(file_name); + + utils::encryption::kdf_config path_cfg; + if (not utils::encryption::kdf_config::from_header(buffer, path_cfg)) { + return false; + } + + auto path_key = path_cfg.recreate_subkey( + utils::encryption::kdf_context::path, master_key); + + file_name.clear(); + return utils::encryption::decrypt_data( + path_key, &buffer[utils::encryption::kdf_config::size()], + buffer.size() - utils::encryption::kdf_config::size(), file_name); + } catch (const std::exception &e) { + utils::error::handle_exception(function_name, e); + } catch (...) { + utils::error::handle_exception(function_name); + } + + return false; +} + auto decrypt_file_path(std::string_view encryption_token, std::string &file_path) -> bool { std::vector decrypted_parts; @@ -49,21 +168,83 @@ auto decrypt_file_path(std::string_view encryption_token, return true; } -auto decrypt_file_name(std::string_view encryption_token, - std::string &file_name) -> bool { - data_buffer buffer; - if (not utils::collection::from_hex_string(file_name, buffer)) { - return false; +auto decrypt_file_path(std::string_view encryption_token, const kdf_config &cfg, + std::string &file_path) -> bool { + std::vector decrypted_parts; + for (const auto &part : std::filesystem::path(file_path)) { + auto file_name = part.string(); + if (file_name == "/") { + continue; + } + + if (not decrypt_file_name(encryption_token, cfg, file_name)) { + return false; + } + + decrypted_parts.push_back(file_name); } - file_name.clear(); - return utils::encryption::decrypt_data(encryption_token, buffer, file_name); + file_path = + utils::path::create_api_path(utils::string::join(decrypted_parts, '/')); + return true; } -auto read_encrypted_range(const http_range &range, - const utils::encryption::hash_256_t &key, - reader_func_t reader_func, std::uint64_t total_size, - data_buffer &data) -> bool { +auto decrypt_file_path(const utils::hash::hash_256_t &master_key, + std::string &file_path) -> bool { + std::vector decrypted_parts; + for (const auto &part : std::filesystem::path(file_path)) { + auto file_name = part.string(); + if (file_name == "/") { + continue; + } + + if (not decrypt_file_name(master_key, file_name)) { + return false; + } + + decrypted_parts.push_back(file_name); + } + + file_path = + utils::path::create_api_path(utils::string::join(decrypted_parts, '/')); + return true; +} + +template +[[nodiscard]] auto +read_encrypted_range(http_range range, const utils::hash::hash_256_t &key, + reader_func_t reader_func, std::uint64_t total_size, + data_t &data, std::uint8_t file_header_size, + std::size_t &bytes_read) -> bool { + bytes_read = 0U; + + { + if (total_size == 0U) { + return true; + } + + std::uint64_t begin = range.begin; + std::uint64_t end = range.end; + + if (begin >= total_size) { + return true; + } + + std::uint64_t last = total_size - 1U; + if (end > last) { + end = last; + } + + if (end < begin) { + return true; + } + + range = http_range{ + .begin = begin, + .end = end, + }; + } + auto encrypted_chunk_size = utils::encryption::encrypting_reader::get_encrypted_chunk_size(); auto data_chunk_size = @@ -75,22 +256,22 @@ auto read_encrypted_range(const http_range &range, auto source_offset = static_cast(range.begin % data_chunk_size); for (std::size_t chunk = start_chunk; chunk <= end_chunk; chunk++) { - data_buffer cypher; - auto start_offset = chunk * encrypted_chunk_size; + data_buffer cipher; + auto start_offset = (chunk * encrypted_chunk_size) + file_header_size; auto end_offset = std::min( start_offset + (total_size - (chunk * data_chunk_size)) + encryption_header_size - 1U, static_cast(start_offset + encrypted_chunk_size - 1U)); - if (not reader_func(cypher, start_offset, end_offset)) { + if (not reader_func(cipher, start_offset, end_offset)) { return false; } data_buffer source_buffer; - if (not utils::encryption::decrypt_data(key, cypher, source_buffer)) { + if (not utils::encryption::decrypt_data(key, cipher, source_buffer)) { return false; } - cypher.clear(); + cipher.clear(); auto data_size = static_cast(std::min( remain, static_cast(data_chunk_size - source_offset))); @@ -98,58 +279,8 @@ auto read_encrypted_range(const http_range &range, static_cast(source_offset)), std::next(source_buffer.begin(), static_cast(source_offset + data_size)), - std::back_inserter(data)); - remain -= data_size; - source_offset = 0U; - } - - return true; -} - -auto read_encrypted_range(const http_range &range, - const utils::encryption::hash_256_t &key, - reader_func_t reader_func, std::uint64_t total_size, - unsigned char *data, std::size_t size, - std::size_t &bytes_read) -> bool { - bytes_read = 0U; - - auto encrypted_chunk_size = - utils::encryption::encrypting_reader::get_encrypted_chunk_size(); - auto data_chunk_size = - utils::encryption::encrypting_reader::get_data_chunk_size(); - - auto start_chunk = static_cast(range.begin / data_chunk_size); - auto end_chunk = static_cast(range.end / data_chunk_size); - auto remain = range.end - range.begin + 1U; - auto source_offset = static_cast(range.begin % data_chunk_size); - - std::span dest_buffer(data, size); - for (std::size_t chunk = start_chunk; chunk <= end_chunk; chunk++) { - data_buffer cypher; - auto start_offset = chunk * encrypted_chunk_size; - auto end_offset = std::min( - start_offset + (total_size - (chunk * data_chunk_size)) + - encryption_header_size - 1U, - static_cast(start_offset + encrypted_chunk_size - 1U)); - - if (not reader_func(cypher, start_offset, end_offset)) { - return false; - } - - data_buffer source_buffer; - if (not utils::encryption::decrypt_data(key, cypher, source_buffer)) { - return false; - } - cypher.clear(); - - auto data_size = static_cast(std::min( - remain, static_cast(data_chunk_size - source_offset))); - std::copy( - std::next(source_buffer.begin(), - static_cast(source_offset)), - std::next(source_buffer.begin(), - static_cast(source_offset + data_size)), - std::next(dest_buffer.begin(), static_cast(bytes_read))); + std::next(resize_by(data, data_size).begin(), + static_cast(bytes_read))); remain -= data_size; bytes_read += data_size; source_offset = 0U; @@ -157,6 +288,26 @@ auto read_encrypted_range(const http_range &range, return true; } + +auto read_encrypted_range(const http_range &range, + const utils::hash::hash_256_t &key, bool uses_kdf, + reader_func_t reader_func, std::uint64_t total_size, + data_buffer &data) -> bool { + std::size_t bytes_read{}; + return read_encrypted_range( + range, key, reader_func, total_size, data, + uses_kdf ? kdf_config::size() : 0U, bytes_read); +} + +[[nodiscard]] auto read_encrypted_range( + const http_range &range, const utils::hash::hash_256_t &key, bool uses_kdf, + reader_func_t reader_func, std::uint64_t total_size, unsigned char *data, + std::size_t size, std::size_t &bytes_read) -> bool { + data_span dest_buffer(data, size); + return read_encrypted_range( + range, key, reader_func, total_size, dest_buffer, + uses_kdf ? kdf_config::size() : 0U, bytes_read); +} } // namespace monitarr::utils::encryption #endif // defined(PROJECT_ENABLE_LIBSODIUM) && defined (PROJECT_ENABLE_BOOST) diff --git a/support/src/utils/file.cpp b/support/src/utils/file.cpp index e488920..f5b1d8c 100644 --- a/support/src/utils/file.cpp +++ b/support/src/utils/file.cpp @@ -53,10 +53,11 @@ auto change_to_process_directory() -> bool { } #else // !defined(_WIN32) std::string path; - path.resize(PATH_MAX + 1); + path.resize(monitarr::max_path_length + 1); #if defined(__APPLE__) - auto res =proc_pidpath(getpid(), reinterpret_cast(path.data()), - static_cast(path.size())); + auto res = proc_pidpath(getpid(), reinterpret_cast(path.data()), + static_cast(path.size())); + path = path.c_str(); #else // !defined(__APPLE__) auto res = readlink("/proc/self/exe", path.data(), path.size()); if (res == -1) { @@ -134,8 +135,8 @@ auto get_free_drive_space(std::string_view path) #endif // defined(_WIN32) #if defined(__linux__) - struct statfs64 st{}; - if (statfs64(std::string{path}.c_str(), &st) != 0) { + struct statfs64 u_stat{}; + if (statfs64(std::string{path}.c_str(), &u_stat) != 0) { throw utils::error::create_exception( function_name, { "failed to get free disk space", @@ -144,12 +145,12 @@ auto get_free_drive_space(std::string_view path) }); } - return st.f_bfree * static_cast(st.f_bsize); + return u_stat.f_bfree * static_cast(u_stat.f_bsize); #endif // defined(__linux__) #if defined(__APPLE__) - struct statvfs st{}; - if (statvfs(std::string{path}.c_str(), &st) != 0) { + struct statvfs u_stat{}; + if (statvfs(std::string{path}.c_str(), &u_stat) != 0) { throw utils::error::create_exception( function_name, { "failed to get free disk space", @@ -158,7 +159,7 @@ auto get_free_drive_space(std::string_view path) }); } - return st.f_bfree * static_cast(st.f_frsize); + return u_stat.f_bfree * static_cast(u_stat.f_frsize); #endif // defined(__APPLE__) } catch (const std::exception &e) { utils::error::handle_exception(function_name, e); @@ -208,6 +209,7 @@ auto get_times(std::string_view path) -> std::optional { if (res) { ret.accessed = utils::time::windows_file_time_to_unix_time(times.at(1U)); + ret.changed = utils::time::windows_file_time_to_unix_time(times.at(2U)); ret.created = utils::time::windows_file_time_to_unix_time(times.at(0U)); ret.modified = utils::time::windows_file_time_to_unix_time(times.at(2U)); @@ -216,8 +218,8 @@ auto get_times(std::string_view path) -> std::optional { } } - struct _stat64 st{}; - if (_stat64(std::string{path}.c_str(), &st) != 0) { + struct _stat64 u_stat{}; + if (_stat64(std::string{path}.c_str(), &u_stat) != 0) { throw utils::error::create_exception( function_name, { "failed to get file times", @@ -226,13 +228,14 @@ auto get_times(std::string_view path) -> std::optional { }); } - ret.accessed = utils::time::windows_time_t_to_unix_time(st.st_atime); - ret.created = utils::time::windows_time_t_to_unix_time(st.st_ctime); - ret.modified = utils::time::windows_time_t_to_unix_time(st.st_mtime); - ret.written = utils::time::windows_time_t_to_unix_time(st.st_mtime); + ret.accessed = utils::time::windows_time_t_to_unix_time(u_stat.st_atime); + ret.changed = utils::time::windows_time_t_to_unix_time(u_stat.st_ctime); + ret.created = utils::time::windows_time_t_to_unix_time(u_stat.st_ctime); + ret.modified = utils::time::windows_time_t_to_unix_time(u_stat.st_mtime); + ret.written = utils::time::windows_time_t_to_unix_time(u_stat.st_mtime); #else // !defined(_WIN32) - struct stat64 st{}; - if (stat64(std::string{path}.c_str(), &st) != 0) { + struct stat64 u_stat{}; + if (stat64(std::string{path}.c_str(), &u_stat) != 0) { throw utils::error::create_exception( function_name, { "failed to get file times", @@ -242,30 +245,36 @@ auto get_times(std::string_view path) -> std::optional { } #if defined(__APPLE__) - ret.accessed = static_cast(st.st_atimespec.tv_nsec) + - static_cast(st.st_atimespec.tv_sec) * + ret.accessed = static_cast(u_stat.st_atimespec.tv_nsec) + + static_cast(u_stat.st_atimespec.tv_sec) * utils::time::NANOS_PER_SECOND; - ret.created = static_cast(st.st_ctimespec.tv_nsec) + - static_cast(st.st_ctimespec.tv_sec) * + ret.created = static_cast(u_stat.st_birthtimespec.tv_nsec) + + static_cast(u_stat.st_birthtimespec.tv_sec) * utils::time::NANOS_PER_SECOND; - ret.modified = static_cast(st.st_mtimespec.tv_nsec) + - static_cast(st.st_mtimespec.tv_sec) * + ret.changed = static_cast(u_stat.st_ctimespec.tv_nsec) + + static_cast(u_stat.st_ctimespec.tv_sec) * + utils::time::NANOS_PER_SECOND; + ret.modified = static_cast(u_stat.st_mtimespec.tv_nsec) + + static_cast(u_stat.st_mtimespec.tv_sec) * utils::time::NANOS_PER_SECOND; - ret.written = static_cast(st.st_mtimespec.tv_nsec) + - static_cast(st.st_mtimespec.tv_sec) * + ret.written = static_cast(u_stat.st_mtimespec.tv_nsec) + + static_cast(u_stat.st_mtimespec.tv_sec) * utils::time::NANOS_PER_SECOND; #else // !defined(__APPLE__) - ret.accessed = static_cast(st.st_atim.tv_nsec) + - static_cast(st.st_atim.tv_sec) * + ret.accessed = static_cast(u_stat.st_atim.tv_nsec) + + static_cast(u_stat.st_atim.tv_sec) * utils::time::NANOS_PER_SECOND; - ret.created = static_cast(st.st_ctim.tv_nsec) + - static_cast(st.st_ctim.tv_sec) * + ret.changed = static_cast(u_stat.st_ctim.tv_nsec) + + static_cast(u_stat.st_ctim.tv_sec) * utils::time::NANOS_PER_SECOND; - ret.modified = static_cast(st.st_mtim.tv_nsec) + - static_cast(st.st_mtim.tv_sec) * + ret.created = static_cast(u_stat.st_ctim.tv_nsec) + + static_cast(u_stat.st_ctim.tv_sec) * + utils::time::NANOS_PER_SECOND; + ret.modified = static_cast(u_stat.st_mtim.tv_nsec) + + static_cast(u_stat.st_mtim.tv_sec) * utils::time::NANOS_PER_SECOND; - ret.written = static_cast(st.st_mtim.tv_nsec) + - static_cast(st.st_mtim.tv_sec) * + ret.written = static_cast(u_stat.st_mtim.tv_nsec) + + static_cast(u_stat.st_mtim.tv_sec) * utils::time::NANOS_PER_SECOND; #endif // defined(__APPLE__) #endif // defined(_WIN32) @@ -305,8 +314,8 @@ auto get_total_drive_space(std::string_view path) #endif // defined(_WIN32) #if defined(__linux__) - struct statfs64 st{}; - if (statfs64(std::string{path}.c_str(), &st) != 0) { + struct statfs64 u_stat{}; + if (statfs64(std::string{path}.c_str(), &u_stat) != 0) { throw utils::error::create_exception( function_name, { "failed to get total disk space", @@ -315,12 +324,12 @@ auto get_total_drive_space(std::string_view path) }); } - return st.f_blocks * static_cast(st.f_bsize); + return u_stat.f_blocks * static_cast(u_stat.f_bsize); #endif // defined(__linux__) #if defined(__APPLE__) - struct statvfs st{}; - if (statvfs(std::string{path}.c_str(), &st) != 0) { + struct statvfs u_stat{}; + if (statvfs(std::string{path}.c_str(), &u_stat) != 0) { throw utils::error::create_exception( function_name, { "failed to get total disk space", @@ -329,7 +338,7 @@ auto get_total_drive_space(std::string_view path) }); } - return st.f_blocks * static_cast(st.f_frsize); + return u_stat.f_blocks * static_cast(u_stat.f_frsize); #endif // defined(__APPLE__) } catch (const std::exception &e) { utils::error::handle_exception(function_name, e); diff --git a/support/src/utils/file_directory.cpp b/support/src/utils/file_directory.cpp index a8e1957..9681cf2 100644 --- a/support/src/utils/file_directory.cpp +++ b/support/src/utils/file_directory.cpp @@ -21,6 +21,7 @@ */ #include "utils/file_directory.hpp" +#include "utils/com_init_wrapper.hpp" #include "utils/common.hpp" #include "utils/error.hpp" #include "utils/string.hpp" @@ -83,19 +84,19 @@ auto traverse_directory( }); } - struct dirent *de{nullptr}; - while (res && (de = readdir(root)) && !is_stop_requested()) { - if (de->d_type == DT_DIR) { - if ((std::string_view(de->d_name) == ".") || - (std::string_view(de->d_name) == "..")) { + struct dirent *entry{nullptr}; + while (res && (entry = ::readdir(root)) && !is_stop_requested()) { + if (entry->d_type == DT_DIR) { + if ((std::string_view(entry->d_name) == ".") || + (std::string_view(entry->d_name) == "..")) { continue; } res = directory_action(monitarr::utils::file::directory( - monitarr::utils::path::combine(path, {de->d_name}))); + monitarr::utils::path::combine(path, {entry->d_name}))); } else { res = file_action(monitarr::utils::file::file( - monitarr::utils::path::combine(path, {de->d_name}))); + monitarr::utils::path::combine(path, {entry->d_name}))); } } @@ -112,16 +113,67 @@ auto directory::copy_to(std::string_view new_path, bool overwrite) const MONITARR_USES_FUNCTION_NAME(); try { - throw utils::error::create_exception( - function_name, { - "failed to copy directory", - "not implemented", - utils::string::from_bool(overwrite), - new_path, - path_, - }); - } catch (const std::exception &e) { - utils::error::handle_exception(function_name, e); + if (not exists()) { + throw utils::error::create_exception(function_name, + { + "failed to copy directory", + "source does not exist", + path_, + std::string{new_path}, + }); + } + + auto src_root = utils::path::finalize(path_); + auto dst_root = utils::path::finalize(new_path); + + if (directory{dst_root}.exists()) { + auto src_base = utils::path::strip_to_file_name(src_root); + dst_root = utils::path::combine(dst_root, {src_base}); + } else { + auto dst_parent = utils::path::get_parent_path(dst_root); + if (not dst_parent.empty() && not directory{dst_parent}.exists()) { + auto parent_parent = utils::path::get_parent_path(dst_parent); + auto last_piece = utils::path::strip_to_file_name(dst_parent); + [[maybe_unused]] auto sub_dir = + directory{parent_parent}.create_directory(last_piece); + } + if (not directory{dst_root}.exists()) { + auto root_parent = utils::path::get_parent_path(dst_root); + auto root_name = utils::path::strip_to_file_name(dst_root); + [[maybe_unused]] auto sub_dir = + directory{root_parent}.create_directory(root_name); + } + } + + auto success = traverse_directory( + src_root, + [this, &dst_root, &src_root](auto &&dir_item) -> bool { + auto child_src = dir_item.get_path(); + auto rel_path = utils::path::get_relative_path(child_src, src_root); + auto child_dst = utils::path::combine(dst_root, {rel_path}); + + auto child_parent = utils::path::get_parent_path(child_dst); + auto child_name = utils::path::strip_to_file_name(child_dst); + [[maybe_unused]] auto sub_dir = + directory{child_parent}.create_directory(child_name); + return not is_stop_requested(); + }, + [this, &dst_root, overwrite, &src_root](auto &&file_item) -> bool { + auto child_src = file_item.get_path(); + auto rel_path = utils::path::get_relative_path(child_src, src_root); + auto child_dst = utils::path::combine(dst_root, {rel_path}); + + if (not file{child_src}.copy_to(child_dst, overwrite)) { + return false; + } + + return not is_stop_requested(); + }, + stop_requested_); + + return success && not is_stop_requested(); + } catch (const std::exception &ex) { + utils::error::handle_exception(function_name, ex); } catch (...) { utils::error::handle_exception(function_name); } @@ -137,7 +189,7 @@ auto directory::count(bool recursive) const -> std::uint64_t { traverse_directory( path_, - [&ret, &recursive](auto dir_item) -> bool { + [&ret, &recursive](auto &&dir_item) -> bool { if (recursive) { ret += dir_item.count(true); } @@ -145,7 +197,7 @@ auto directory::count(bool recursive) const -> std::uint64_t { ++ret; return true; }, - [&ret](auto /* file_item */) -> bool { + [&ret](auto && /* file_item */) -> bool { ++ret; return true; }, @@ -172,6 +224,8 @@ auto directory::create_directory(std::string_view path) const } #if defined(_WIN32) + [[maybe_unused]] thread_local const utils::com_init_wrapper wrapper; + auto res = ::SHCreateDirectory(nullptr, utils::string::from_utf8(abs_path).c_str()); if (res != ERROR_SUCCESS) { @@ -215,8 +269,8 @@ auto directory::exists() const -> bool { #if defined(_WIN32) return ::PathIsDirectoryA(path_.c_str()) != 0; #else // !defined(_WIN32) - struct stat64 st{}; - return (stat64(path_.c_str(), &st) == 0 && S_ISDIR(st.st_mode)); + struct stat64 u_stat{}; + return (stat64(path_.c_str(), &u_stat) == 0 && S_ISDIR(u_stat.st_mode)); #endif // defined(_WIN32) return false; @@ -249,14 +303,14 @@ auto directory::get_directories() const -> std::vector { traverse_directory( path_, - [this, &ret](auto dir_item) -> bool { + [this, &ret](auto &&dir_item) -> bool { ret.emplace_back(fs_directory_t{ new directory(dir_item.get_path(), stop_requested_), }); return true; }, - [](auto /* file_item */) -> bool { return true; }, stop_requested_); + [](auto && /* file_item */) -> bool { return true; }, stop_requested_); return ret; } catch (const std::exception &e) { @@ -308,8 +362,8 @@ auto directory::get_files() const -> std::vector { std::vector ret{}; traverse_directory( - path_, [](auto /* dir_item */) -> bool { return true; }, - [&ret](auto file_item) -> bool { + path_, [](auto && /* dir_item */) -> bool { return true; }, + [&ret](auto &&file_item) -> bool { ret.emplace_back(fs_file_t{ new file(file_item.get_path()), }); @@ -335,13 +389,13 @@ auto directory::get_items() const -> std::vector { traverse_directory( path_, - [this, &ret](auto dir_item) -> bool { + [this, &ret](auto &&dir_item) -> bool { ret.emplace_back(fs_item_t{ new directory(dir_item.get_path(), stop_requested_), }); return true; }, - [&ret](auto file_item) -> bool { + [&ret](auto &&file_item) -> bool { ret.emplace_back(fs_item_t{ new file(file_item.get_path()), }); @@ -381,15 +435,68 @@ auto directory::move_to(std::string_view new_path) -> bool { MONITARR_USES_FUNCTION_NAME(); try { - throw utils::error::create_exception(function_name, - { - "failed to move directory", - "not implemented", - new_path, - path_, - }); - } catch (const std::exception &e) { - utils::error::handle_exception(function_name, e); + if (not exists()) { + return true; + } + + auto src_root = utils::path::finalize(path_); + auto dst_root = utils::path::finalize(new_path); + if (src_root == dst_root) { + return true; + } + + if (directory{dst_root}.exists()) { + throw utils::error::create_exception(function_name, + { + "failed to move directory", + "destination exists", + src_root, + dst_root, + }); + } + + auto dst_parent = utils::path::get_parent_path(dst_root); + if (not dst_parent.empty() && not directory{dst_parent}.exists()) { + auto parent_parent = utils::path::get_parent_path(dst_parent); + auto last_piece = utils::path::strip_to_file_name(dst_parent); + [[maybe_unused]] auto sub_dir = + directory{parent_parent}.create_directory(last_piece); + } + + if (not directory{dst_root}.exists()) { + auto root_parent = utils::path::get_parent_path(dst_root); + auto root_name = utils::path::strip_to_file_name(dst_root); + [[maybe_unused]] auto sub_dir = + directory{root_parent}.create_directory(root_name); + } + + if (not copy_to(dst_root, true)) { + throw utils::error::create_exception(function_name, + { + "failed to move directory", + "copy failed", + src_root, + dst_root, + }); + } + + if (is_stop_requested()) { + return false; + } + + if (not remove_recursively()) { + throw utils::error::create_exception(function_name, + { + "failed to move directory", + "remove source failed", + src_root, + }); + } + + path_ = dst_root; + return true; + } catch (const std::exception &ex) { + utils::error::handle_exception(function_name, ex); } catch (...) { utils::error::handle_exception(function_name); } @@ -436,8 +543,10 @@ auto directory::remove_recursively() -> bool { if (not traverse_directory( path_, - [](auto dir_item) -> bool { return dir_item.remove_recursively(); }, - [](auto file_item) -> bool { return file_item.remove(); }, + [](auto &&dir_item) -> bool { + return dir_item.remove_recursively(); + }, + [](auto &&file_item) -> bool { return file_item.remove(); }, stop_requested_)) { return false; } @@ -460,14 +569,14 @@ auto directory::size(bool recursive) const -> std::uint64_t { traverse_directory( path_, - [&ret, &recursive](auto dir_item) -> bool { + [&ret, &recursive](auto &&dir_item) -> bool { if (recursive) { ret += dir_item.size(true); } return true; }, - [&ret](auto file_item) -> bool { + [&ret](auto &&file_item) -> bool { auto cur_size = file_item.size(); if (cur_size.has_value()) { ret += cur_size.value(); diff --git a/support/src/utils/file_enc_file.cpp b/support/src/utils/file_enc_file.cpp index 47e8bc1..6991936 100644 --- a/support/src/utils/file_enc_file.cpp +++ b/support/src/utils/file_enc_file.cpp @@ -43,7 +43,7 @@ auto enc_file::copy_to(std::string_view new_path, bool overwrite) const return file_->copy_to(new_path, overwrite); } -void enc_file::flush() const { return file_->flush(); } +void enc_file::flush() const { file_->flush(); } auto enc_file::move_to(std::string_view path) -> bool { return file_->move_to(path); @@ -68,8 +68,8 @@ auto enc_file::read(unsigned char *data, std::size_t to_read, std::size_t bytes_read{}; auto ret{ utils::encryption::read_encrypted_range( - {offset, offset + to_read - 1U}, - utils::encryption::generate_key( + {.begin = offset, .end = offset + to_read - 1U}, + utils::encryption::generate_key( encryption_token_), [&](auto &&ct_buffer, auto &&start_offset, auto &&end_offset) -> bool { @@ -145,7 +145,7 @@ auto enc_file::truncate(std::size_t size) -> bool { return i_file::write(data, offset); } - auto begin_chunk{ + /* auto begin_chunk{ file_size.value() / utils::encryption::encrypting_reader::get_data_chunk_size(), }; @@ -153,13 +153,14 @@ auto enc_file::truncate(std::size_t size) -> bool { utils::divide_with_ceiling( file_size.value(), utils::encryption::encrypting_reader::get_data_chunk_size()), - }; + }; */ return false; } -auto enc_file::write(const unsigned char *data, std::size_t to_write, - std::size_t offset, std::size_t *total_written) -> bool { +auto enc_file::write(const unsigned char * /* data */, std::size_t to_write, + std::size_t offset, std::size_t * /* total_written */) + -> bool { auto file_size{size()}; if (not file_size.has_value()) { return false; @@ -181,7 +182,7 @@ auto enc_file::size() const -> std::optional { } return utils::encryption::encrypting_reader::calculate_decrypted_size( - file_size.value()); + file_size.value(), false); } } // namespace monitarr::utils::file diff --git a/support/src/utils/file_file.cpp b/support/src/utils/file_file.cpp index 5038cca..8051066 100644 --- a/support/src/utils/file_file.cpp +++ b/support/src/utils/file_file.cpp @@ -33,13 +33,13 @@ namespace { file_size = 0U; #if defined(_WIN32) - struct _stat64 st{}; - auto res = _stat64(std::string{path}.c_str(), &st); + struct _stat64 u_stat{}; + auto res = _stat64(std::string{path}.c_str(), &u_stat); if (res != 0) { return false; } - file_size = static_cast(st.st_size); + file_size = static_cast(u_stat.st_size); return true; #else // !defined(_WIN32) std::error_code ec{}; @@ -55,8 +55,9 @@ namespace { return ((::PathFileExistsA(abs_path.c_str()) != 0) && (::PathIsDirectoryA(abs_path.c_str()) == 0)); #else // !defined(_WIN32) - struct stat64 st{}; - return (stat64(abs_path.c_str(), &st) == 0 && not S_ISDIR(st.st_mode)); + struct stat64 u_stat{}; + return (stat64(abs_path.c_str(), &u_stat) == 0 && + not S_ISDIR(u_stat.st_mode)); #endif // defined(_WIN32) } } // namespace @@ -265,7 +266,7 @@ auto file::move_to(std::string_view path) -> bool { #if defined(_WIN32) success = ::MoveFileExA(path_.c_str(), abs_path.c_str(), MOVEFILE_REPLACE_EXISTING) != 0; -#else // !// defined(_WIN32) +#else // !defined(_WIN32) std::error_code ec{}; std::filesystem::rename(path_, abs_path, ec); success = ec.value() == 0; diff --git a/support/src/utils/hash.cpp b/support/src/utils/hash.cpp index f1c7e3e..1c48f90 100644 --- a/support/src/utils/hash.cpp +++ b/support/src/utils/hash.cpp @@ -25,7 +25,58 @@ #include "utils/error.hpp" -namespace monitarr::utils::encryption { +namespace monitarr::utils::hash { +auto create_hash_blake2b_32(std::string_view data) -> hash_32_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), data.size()); +} + +auto create_hash_blake2b_32(std::wstring_view data) -> hash_32_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), + data.size() * sizeof(wchar_t)); +} + +auto create_hash_blake2b_32(const data_buffer &data) -> hash_32_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), + data.size() * sizeof(data_buffer::value_type)); +} + +auto create_hash_blake2b_64(std::string_view data) -> hash_64_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), data.size()); +} + +auto create_hash_blake2b_64(std::wstring_view data) -> hash_64_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), + data.size() * sizeof(wchar_t)); +} + +auto create_hash_blake2b_64(const data_buffer &data) -> hash_64_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), + data.size() * sizeof(data_buffer::value_type)); +} + +auto create_hash_blake2b_128(std::string_view data) -> hash_128_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), data.size()); +} + +auto create_hash_blake2b_128(std::wstring_view data) -> hash_128_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), + data.size() * sizeof(wchar_t)); +} + +auto create_hash_blake2b_128(const data_buffer &data) -> hash_128_t { + return create_hash_blake2b_t( + reinterpret_cast(data.data()), + data.size() * sizeof(data_buffer::value_type)); +} + auto create_hash_blake2b_256(std::string_view data) -> hash_256_t { return create_hash_blake2b_t( reinterpret_cast(data.data()), data.size()); @@ -111,8 +162,8 @@ auto create_hash_sha512(const data_buffer &data) -> hash_512_t { data.size() * sizeof(data_buffer::value_type)); } -auto create_hash_sha512(const unsigned char *data, - std::size_t data_size) -> hash_512_t { +auto create_hash_sha512(const unsigned char *data, std::size_t data_size) + -> hash_512_t { MONITARR_USES_FUNCTION_NAME(); hash_512_t hash{}; @@ -148,8 +199,8 @@ auto create_hash_sha512(const unsigned char *data, return hash; } -auto create_hash_sha256(const unsigned char *data, - std::size_t data_size) -> hash_256_t { +auto create_hash_sha256(const unsigned char *data, std::size_t data_size) + -> hash_256_t { MONITARR_USES_FUNCTION_NAME(); hash_256_t hash{}; @@ -184,6 +235,6 @@ auto create_hash_sha256(const unsigned char *data, return hash; } -} // namespace monitarr::utils::encryption +} // namespace monitarr::utils::hash #endif // defined(PROJECT_ENABLE_LIBSODIUM) diff --git a/support/src/utils/string.cpp b/support/src/utils/string.cpp index 0d376ab..e83af4c 100644 --- a/support/src/utils/string.cpp +++ b/support/src/utils/string.cpp @@ -36,10 +36,49 @@ auto from_dynamic_bitset(const boost::dynamic_bitset<> &bitset) -> std::string { #endif // defined(PROJECT_ENABLE_BOOST) auto from_utf8(std::string_view str) -> std::wstring { - return str.empty() - ? L"" - : std::wstring_convert, wchar_t>() - .from_bytes(std::string{str}); + if (str.empty()) { + return L""; + } + + std::wstring out; + + const auto *str_ptr = reinterpret_cast(str.data()); + std::int32_t idx{}; + auto len{static_cast(str.size())}; + +#if WCHAR_MAX <= 0xFFFF + out.reserve((str.size() + 1U) / 2U); + while (idx < len) { + UChar32 uni_ch{}; + U8_NEXT(str_ptr, idx, len, uni_ch); + if (uni_ch < 0 || not U_IS_UNICODE_CHAR(uni_ch)) { + throw std::runtime_error("from_utf8: invalid UTF-8 sequence"); + } + std::array units{}; + std::int32_t off{}; + auto err{false}; + U16_APPEND(units.data(), off, 2, uni_ch, err); + if (err || off <= 0) { + throw std::runtime_error("from_utf8: U16_APPEND failed"); + } + out.push_back(static_cast(units[0U])); + if (off == 2) { + out.push_back(static_cast(units[1U])); + } + } +#else // WCHAR_MAX > 0xFFFF + out.reserve(str.size()); + while (idx < len) { + UChar32 uni_ch{}; + U8_NEXT(str_ptr, idx, len, uni_ch); + if (uni_ch < 0 || not U_IS_UNICODE_CHAR(uni_ch)) { + throw std::runtime_error("from_utf8: invalid UTF-8 sequence"); + } + out.push_back(static_cast(uni_ch)); + } +#endif // WCHAR_MAX <= 0xFFFF + + return out; } #if defined(PROJECT_ENABLE_SFML) @@ -55,8 +94,8 @@ auto replace_sf(sf::String &src, const sf::String &find, const sf::String &with, return src; } -auto split_sf(sf::String str, wchar_t delim, - bool should_trim) -> std::vector { +auto split_sf(sf::String str, wchar_t delim, bool should_trim) + -> std::vector { auto result = std::views::split(str.toWideString(), delim); std::vector ret{}; @@ -130,9 +169,51 @@ auto to_uint64(const std::string &val) -> std::uint64_t { auto to_utf8(std::string_view str) -> std::string { return std::string{str}; } auto to_utf8(std::wstring_view str) -> std::string { - return str.empty() - ? "" - : std::wstring_convert, wchar_t>() - .to_bytes(std::wstring{str}); + if (str.empty()) { + return ""; + } + + std::string out; + out.reserve(static_cast(str.size()) * 4); + +#if WCHAR_MAX <= 0xFFFF + const auto *u16 = reinterpret_cast(str.data()); + std::int32_t idx{}; + auto len{static_cast(str.size())}; + while (idx < len) { + UChar32 uni_ch{}; + U16_NEXT(u16, idx, len, uni_ch); + if (uni_ch < 0 || not U_IS_UNICODE_CHAR(uni_ch)) { + throw std::runtime_error("to_utf8: invalid UTF-16 sequence"); + } + std::array buf{}; + std::int32_t off{0}; + auto err{false}; + U8_APPEND(buf, off, U8_MAX_LENGTH, uni_ch, err); + if (err || off <= 0) { + throw std::runtime_error("to_utf8: U8_APPEND failed"); + } + out.append(reinterpret_cast(buf.data()), + static_cast(off)); + } +#else // WCHAR_MAX > 0xFFFF + for (const auto &cur_ch : str) { + auto uni_char{static_cast(cur_ch)}; + if (not U_IS_UNICODE_CHAR(uni_char)) { + throw std::runtime_error("to_utf8: invalid Unicode scalar value"); + } + std::array buf{}; + std::int32_t off{0}; + auto err{false}; + U8_APPEND(buf, off, U8_MAX_LENGTH, uni_char, err); + if (err || off <= 0) { + throw std::runtime_error("to_utf8: U8_APPEND failed"); + } + out.append(reinterpret_cast(buf.data()), + static_cast(off)); + } +#endif // WCHAR_MAX <= 0xFFFF + + return out; } } // namespace monitarr::utils::string diff --git a/support/src/utils/timeout.cpp b/support/src/utils/timeout.cpp new file mode 100644 index 0000000..60a8f5a --- /dev/null +++ b/support/src/utils/timeout.cpp @@ -0,0 +1,82 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include "utils/timeout.hpp" + +namespace monitarr::utils { +timeout::timeout(callback_t timeout_callback, + std::chrono::system_clock::duration duration) + : duration_(duration), + timeout_callback_(std::move(timeout_callback)), + timeout_killed_(duration <= std::chrono::system_clock::duration::zero()) { + if (timeout_killed_) { + return; + } + + timeout_thread_ = std::make_unique([this]() { + std::unique_lock loc_lock(timeout_mutex_); + + while (not timeout_killed_) { + auto res = timeout_notify_.wait_for(loc_lock, duration_); + if (res != std::cv_status::timeout) { + continue; + } + + if (timeout_killed_) { + return; + } + + timeout_killed_ = true; + loc_lock.unlock(); + + try { + timeout_callback_(); + } catch (...) { + } + return; + } + }); +} + +timeout::~timeout() { disable(); } + +void timeout::disable() { + unique_mutex_lock lock(timeout_mutex_); + std::unique_ptr timeout_thread{nullptr}; + std::swap(timeout_thread, timeout_thread_); + + if (not timeout_thread) { + timeout_notify_.notify_all(); + return; + } + + timeout_killed_ = true; + timeout_notify_.notify_all(); + lock.unlock(); + + timeout_thread->join(); +} + +void timeout::reset() { + mutex_lock lock(timeout_mutex_); + timeout_notify_.notify_all(); +} +} // namespace monitarr::utils diff --git a/support/src/utils/unix.cpp b/support/src/utils/unix.cpp index 407de95..64abf6d 100644 --- a/support/src/utils/unix.cpp +++ b/support/src/utils/unix.cpp @@ -22,18 +22,236 @@ #if !defined(_WIN32) #include "utils/unix.hpp" + #include "utils/collection.hpp" +#include "utils/config.hpp" +#include "utils/error.hpp" +#include "utils/file.hpp" +#include "utils/path.hpp" + +namespace { +[[nodiscard]] auto get_group_list(auto *pass) -> std::vector { + MONITARR_USES_FUNCTION_NAME(); + + std::vector groups{}; +#if defined(__APPLE__) + constexpr int buffer_count{8}; + constexpr int max_group_count{1024}; + groups.resize(buffer_count); + + std::size_t orig_count{0U}; + while (true) { + auto group_count{static_cast(groups.size())}; + if (group_count > max_group_count) { + monitarr::utils::error::handle_error(function_name, + "group list has too many groups"); + break; + } + + auto res{ + getgrouplist(pass->pw_name, static_cast(pass->pw_gid), + reinterpret_cast(groups.data()), &group_count), + }; + if (res < 0) { + if (orig_count == 0U) { + monitarr::utils::error::handle_error( + function_name, std::string{"failed to get group list|error|"} + + std::to_string(errno)); + } + + break; + } + + groups.resize(static_cast(group_count)); + if (groups.size() == orig_count) { + break; + } + + orig_count = groups.size(); + } +#else // !defined(__APPLE__) + int group_count{}; + auto res = getgrouplist(pass->pw_name, pass->pw_gid, nullptr, &group_count); + if (res >= 0) { + monitarr::utils::error::handle_error( + function_name, std::string{"failed to get group list count|error|"} + + std::to_string(errno)); + } + + groups.resize(static_cast(group_count)); + res = getgrouplist(pass->pw_name, pass->pw_gid, groups.data(), &group_count); + if (res >= 0) { + monitarr::utils::error::handle_error( + function_name, + std::string{"failed to get group list|error|"} + std::to_string(errno)); + } +#endif // !defined(__APPLE__) + + return groups; +} + +#if defined(__linux__) +[[nodiscard]] auto sanitize_basename(std::string_view app_name) -> std::string { + std::string out; + out.reserve(app_name.size()); + for (const auto &cur_ch : app_name) { + if ((cur_ch >= 'a' && cur_ch <= 'z') || (cur_ch >= '0' && cur_ch <= '9') || + (cur_ch == '-' || cur_ch == '_')) { + out.push_back(cur_ch); + } else if (cur_ch >= 'A' && cur_ch <= 'Z') { + out.push_back(static_cast(cur_ch - 'A' + 'a')); + } else { + out.push_back('-'); // replace spaces/symbols + } + } + + std::string collapsed; + collapsed.reserve(out.size()); + bool prev_dash = false; + for (const auto &cur_ch : out) { + if (cur_ch == '-') { + if (not prev_dash) { + collapsed.push_back(cur_ch); + } + prev_dash = true; + } else { + collapsed.push_back(cur_ch); + prev_dash = false; + } + } + + if (collapsed.empty()) { + collapsed = "app"; + } + return collapsed; +} + +[[nodiscard]] auto get_autostart_dir() -> std::string { + auto config = monitarr::utils::get_environment_variable("XDG_CONFIG_HOME"); + if (config.empty()) { + config = monitarr::utils::path::combine( + monitarr::utils::get_environment_variable("HOME"), {".config"}); + } + + return monitarr::utils::path::combine(config, {"autostart"}); +} + +[[nodiscard]] auto desktop_file_path_for(std::string_view app_name) + -> std::string { + return monitarr::utils::path::combine( + get_autostart_dir(), { + sanitize_basename(app_name) + ".desktop", + }); +} + +[[nodiscard]] auto join_args_for_exec(const std::vector &args) + -> std::string { + std::string str; + for (const auto &arg : args) { + if (not str.empty()) { + str += ' '; + } + + auto needs_quotes = arg.find_first_of(" \t\"'\\$`") != std::string::npos; + if (needs_quotes) { + str += '"'; + for (const auto &cur_ch : arg) { + if (cur_ch == '"' || cur_ch == '\\') { + str += '\\'; + } + str += cur_ch; + } + str += '"'; + } else { + str += arg; + } + } + + return str; +} +#endif // defined(__linux__) +} // namespace namespace monitarr::utils { -#if defined(__APPLE__) -auto convert_to_uint64(pthread_t thread) -> std::uint64_t { - return reinterpret_cast(thread); -} -#else // defined(__APPLE__) +#if !defined(__APPLE__) auto convert_to_uint64(const pthread_t &thread) -> std::uint64_t { return static_cast(thread); } -#endif // defined(__APPLE__) +#endif // !defined(__APPLE__) + +#if defined(__linux__) +auto create_autostart_entry(const autostart_cfg &cfg, bool overwrite_existing) + -> bool { + MONITARR_USES_FUNCTION_NAME(); + + auto file = desktop_file_path_for(cfg.app_name); + if (utils::file::file{file}.exists() && not overwrite_existing) { + return true; + } + + auto dir = get_autostart_dir(); + if (dir.empty()) { + return false; + } + + if (not utils::file::directory{dir}.create_directory()) { + return false; + } + + auto exec_line = cfg.exec_path; + if (not cfg.exec_args.empty()) { + exec_line += ' '; + exec_line += join_args_for_exec(cfg.exec_args); + } + + std::ofstream out(file, std::ios::binary | std::ios::trunc); + if (not out) { + return false; + } + + out << "[Desktop Entry]\n"; + out << "Type=Application\n"; + out << "Version=1.0\n"; + out << "Name=" << cfg.app_name << "\n"; + out << "Exec=" << exec_line << "\n"; + out << "Terminal=" << (cfg.terminal ? "true" : "false") << "\n"; + + if (cfg.comment && not cfg.comment->empty()) { + out << "Comment=" << *cfg.comment << "\n"; + } + + if (cfg.icon_path && not cfg.icon_path->empty()) { + out << "Icon=" << *cfg.icon_path << "\n"; + } + + if (not cfg.only_show_in.empty()) { + out << "OnlyShowIn="; + for (std::size_t idx = 0U; idx < cfg.only_show_in.size(); ++idx) { + if (idx != 0U) { + out << ';'; + } + out << cfg.only_show_in[idx]; + } + out << ";\n"; + } + + if (not cfg.enabled) { + out << "X-GNOME-Autostart-enabled=false\n"; + out << "Hidden=true\n"; + } + + out.flush(); + if (not out) { + return false; + } + +#if defined(__linux__) || defined(__APPLE__) + chmod(file.c_str(), 0644); +#endif // defined(__linux__) || defined(__APPLE__) + + return true; +} +#endif // defined(__linux__) auto get_last_error_code() -> int { return errno; } @@ -42,20 +260,11 @@ auto get_thread_id() -> std::uint64_t { } auto is_uid_member_of_group(uid_t uid, gid_t gid) -> bool { - std::vector groups{}; - auto res = use_getpwuid(uid, [&groups](struct passwd *pass) { - int group_count{}; - if (getgrouplist(pass->pw_name, pass->pw_gid, nullptr, &group_count) < 0) { - groups.resize(static_cast(group_count)); -#if defined(__APPLE__) - getgrouplist(pass->pw_name, pass->pw_gid, - reinterpret_cast(groups.data()), &group_count); -#else // !defined(__APPLE__) - getgrouplist(pass->pw_name, pass->pw_gid, groups.data(), &group_count); -#endif // defined(__APPLE__) - } - }); + MONITARR_USES_FUNCTION_NAME(); + std::vector groups{}; + auto res = use_getpwuid( + uid, [&groups](struct passwd *pass) { groups = get_group_list(pass); }); if (not res) { throw utils::error::create_exception(res.function_name, {"use_getpwuid failed", res.reason}); @@ -64,6 +273,19 @@ auto is_uid_member_of_group(uid_t uid, gid_t gid) -> bool { return collection::includes(groups, gid); } +#if defined(__linux__) +auto remove_autostart_entry(std::string_view name) -> bool { + MONITARR_USES_FUNCTION_NAME(); + + auto file = desktop_file_path_for(name); + if (not utils::file::file{file}.exists()) { + return true; + } + + return utils::file::file{file}.remove(); +} +#endif // defined(__linux__) + auto use_getpwuid(uid_t uid, passwd_callback_t callback) -> result { MONITARR_USES_FUNCTION_NAME(); @@ -73,15 +295,130 @@ auto use_getpwuid(uid_t uid, passwd_callback_t callback) -> result { auto *temp_pw = getpwuid(uid); if (temp_pw == nullptr) { return { - std::string{function_name}, - false, - "'getpwuid' returned nullptr", + .function_name = std::string{function_name}, + .ok = false, + .reason = "'getpwuid' returned nullptr", }; } callback(temp_pw); - return {std::string{function_name}}; + return { + .function_name = std::string{function_name}, + }; } + +#if defined(__APPLE__) +#if defined(PROJECT_ENABLE_PUGIXML) +auto generate_launchd_plist(const plist_cfg &cfg, bool overwrite_existing) + -> bool { + auto file = utils::path::combine(cfg.plist_path, {cfg.label + ".plist"}); + if (utils::file::file{file}.exists() && not overwrite_existing) { + return true; + } + + pugi::xml_document doc; + auto decl = doc.append_child(pugi::node_declaration); + decl.append_attribute("version") = "1.0"; + decl.append_attribute("encoding") = "UTF-8"; + + auto plist = doc.append_child("plist"); + plist.append_attribute("version") = "1.0"; + + auto dict = plist.append_child("dict"); + + dict.append_child("key").text().set("Label"); + dict.append_child("string").text().set(cfg.label.c_str()); + + dict.append_child("key").text().set("ProgramArguments"); + auto array = dict.append_child("array"); + for (const auto &arg : cfg.args) { + array.append_child("string").text().set(arg.c_str()); + } + + dict.append_child("key").text().set("EnvironmentVariables"); + pugi::xml_node env_dict = dict.append_child("dict"); + if (not utils::get_environment_variable("PROJECT_TEST_CONFIG_DIR").empty()) { + env_dict.append_child("key").text().set("PROJECT_TEST_CONFIG_DIR"); + env_dict.append_child("string").text().set( + utils::get_environment_variable("PROJECT_TEST_CONFIG_DIR")); + } + if (not utils::get_environment_variable("PROJECT_TEST_INPUT_DIR").empty()) { + env_dict.append_child("key").text().set("PROJECT_TEST_INPUT_DIR"); + env_dict.append_child("string").text().set( + utils::get_environment_variable("PROJECT_TEST_INPUT_DIR")); + } + + dict.append_child("key").text().set("WorkingDirectory"); + dict.append_child("string").text().set(cfg.working_dir.c_str()); + + dict.append_child("key").text().set("KeepAlive"); + dict.append_child(cfg.keep_alive ? "true" : "false"); + + dict.append_child("key").text().set("RunAtLoad"); + dict.append_child(cfg.run_at_load ? "true" : "false"); + + dict.append_child("key").text().set("StandardOutPath"); + dict.append_child("string").text().set(cfg.stdout_log.c_str()); + + dict.append_child("key").text().set("StandardErrorPath"); + dict.append_child("string").text().set(cfg.stderr_log.c_str()); + + return doc.save_file(file.c_str(), " ", + pugi::format_indent | pugi::format_write_bom); +} +#endif // defined(PROJECT_ENABLE_PUGIXML) + +#if defined(PROJECT_ENABLE_SPDLOG) || defined(PROJECT_ENABLE_FMT) +auto launchctl_command(std::string_view label, launchctl_type type) -> int { + switch (type) { + case launchctl_type::bootout: + return system( + fmt::format("launchctl bootout gui/{} '{}' 1>/dev/null 2>&1", getuid(), + utils::path::combine("~", + { + "/Library/LaunchAgents", + fmt::format("{}.plist", label), + })) + .c_str()); + + case launchctl_type::bootstrap: + return system( + fmt::format("launchctl bootstrap gui/{} '{}' 1>/dev/null 2>&1", + getuid(), + utils::path::combine("~", + { + "/Library/LaunchAgents", + fmt::format("{}.plist", label), + })) + .c_str()); + + case launchctl_type::kickstart: + return system( + fmt::format("launchctl kickstart gui/{}/{}", getuid(), label).c_str()); + } + + return -1; +} + +auto remove_launchd_plist(std::string_view plist_path, std::string_view label, + bool should_bootout) -> bool { + auto file = utils::file::file{ + utils::path::combine(plist_path, {std::string{label} + ".plist"}), + }; + if (not file.exists()) { + return true; + } + + auto res = + should_bootout ? launchctl_command(label, launchctl_type::bootout) : 0; + if (not file.remove()) { + return false; + } + + return res == 0; +} +#endif // defined(PROJECT_ENABLE_SPDLOG) || defined(PROJECT_ENABLE_FMT) +#endif // defined(__APPLE__) } // namespace monitarr::utils #endif // !defined(_WIN32) diff --git a/support/src/utils/windows.cpp b/support/src/utils/windows.cpp index 6d001a0..f5703a0 100644 --- a/support/src/utils/windows.cpp +++ b/support/src/utils/windows.cpp @@ -25,8 +25,18 @@ #include "utils/com_init_wrapper.hpp" #include "utils/error.hpp" +#include "utils/file.hpp" +#include "utils/path.hpp" #include "utils/string.hpp" +namespace { +constexpr std::array drive_letters{ + "a:", "b:", "c:", "d:", "e:", "f:", "g:", "h:", "i:", + "j:", "k:", "l:", "m:", "n:", "o:", "p:", "q:", "r:", + "s:", "t:", "u:", "v:", "w:", "x:", "y:", "z:", +}; +} + namespace monitarr::utils { void create_console() { if (::AllocConsole() == 0) { @@ -59,13 +69,55 @@ void create_console() { void free_console() { ::FreeConsole(); } +auto get_available_drive_letter(char first) -> std::optional { + const auto *begin = std::ranges::find_if( + drive_letters, [first](auto &&val) { return val.at(0U) == first; }); + if (begin == drive_letters.end()) { + begin = drive_letters.begin(); + } + + auto available = + std::ranges::find_if(begin, drive_letters.end(), [](auto &&val) -> bool { + return not utils::file::directory{utils::path::combine(val, {"\\"})} + .exists(); + }); + if (available == drive_letters.end()) { + return std::nullopt; + } + + return *available; +} + +auto get_available_drive_letters(char first) -> std::vector { + const auto *begin = + std::ranges::find_if(drive_letters, [first](auto &&val) -> bool { + return val.at(0U) == first; + }); + if (begin == drive_letters.end()) { + begin = drive_letters.begin(); + } + + return std::accumulate( + begin, drive_letters.end(), std::vector(), + [](auto &&vec, auto &&letter) -> auto { + if (utils::file::directory{utils::path::combine(letter, {"\\"})} + .exists()) { + return vec; + } + + vec.emplace_back(letter); + return vec; + }); +} + auto get_last_error_code() -> DWORD { return ::GetLastError(); } auto get_local_app_data_directory() -> const std::string & { MONITARR_USES_FUNCTION_NAME(); + [[maybe_unused]] thread_local const utils::com_init_wrapper wrapper; + static std::string app_data = ([]() -> std::string { - com_init_wrapper cw; PWSTR local_app_data{}; if (SUCCEEDED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &local_app_data))) { @@ -91,10 +143,11 @@ auto is_process_elevated() -> bool { auto ret{false}; HANDLE token{INVALID_HANDLE_VALUE}; if (::OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { - TOKEN_ELEVATION te{}; - DWORD sz = sizeof(te); - if (::GetTokenInformation(token, TokenElevation, &te, sizeof(te), &sz)) { - ret = (te.TokenIsElevated != 0); + TOKEN_ELEVATION token_elevation{}; + DWORD size = sizeof(token_elevation); + if (::GetTokenInformation(token, TokenElevation, &token_elevation, + sizeof(token_elevation), &size)) { + ret = (token_elevation.TokenIsElevated != 0); } } @@ -139,6 +192,170 @@ auto run_process_elevated(std::vector args) -> int { } void set_last_error_code(DWORD error_code) { ::SetLastError(error_code); } + +auto get_startup_folder() -> std::wstring { + [[maybe_unused]] thread_local const utils::com_init_wrapper wrapper; + + PWSTR raw{nullptr}; + auto result = ::SHGetKnownFolderPath(FOLDERID_Startup, 0, nullptr, &raw); + if (FAILED(result)) { + if (raw != nullptr) { + ::CoTaskMemFree(raw); + } + + return {}; + } + + std::wstring str{raw}; + ::CoTaskMemFree(raw); + return str; +} + +auto create_shortcut(const shortcut_cfg &cfg, bool overwrite_existing) -> bool { + MONITARR_USES_FUNCTION_NAME(); + + [[maybe_unused]] thread_local const utils::com_init_wrapper wrapper; + + const auto hr_hex = [](HRESULT result) -> std::string { + std::ostringstream oss; + oss << "0x" << std::uppercase << std::hex << std::setw(8) + << std::setfill('0') << static_cast(result); + return oss.str(); + }; + + if (cfg.location.empty()) { + utils::error::handle_error(function_name, "shortcut location was empty"); + return false; + } + + if (not utils::file::directory{cfg.location}.create_directory()) { + utils::error::handle_error(function_name, + "failed to create shortcut directory|path|" + + utils::string::to_utf8(cfg.location)); + return false; + } + + auto final_name = cfg.shortcut_name.empty() + ? utils::path::strip_to_file_name(cfg.exe_path) + : cfg.shortcut_name; + if (not final_name.ends_with(L".lnk")) { + final_name += L".lnk"; + } + + auto lnk_path = utils::path::combine(cfg.location, {final_name}); + if (utils::file::file{lnk_path}.exists() && not overwrite_existing) { + return true; + } + + IShellLinkW *psl{nullptr}; + auto result = ::CoCreateInstance(CLSID_ShellLink, nullptr, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&psl)); + if (FAILED(result)) { + utils::error::handle_error( + function_name, + std::string("CoCreateInstance(CLSID_ShellLink) failed: ") + + hr_hex(result)); + return false; + } + + result = psl->SetPath(cfg.exe_path.c_str()); + if (FAILED(result)) { + utils::error::handle_error(function_name, + std::string("IShellLink::SetPath failed: ") + + hr_hex(result)); + psl->Release(); + return false; + } + + if (not cfg.arguments.empty()) { + result = psl->SetArguments(cfg.arguments.c_str()); + if (FAILED(result)) { + utils::error::handle_error( + function_name, + std::string("IShellLink::SetArguments failed: ") + hr_hex(result)); + psl->Release(); + return false; + } + } + + if (not cfg.working_directory.empty()) { + result = psl->SetWorkingDirectory(cfg.working_directory.c_str()); + if (FAILED(result)) { + utils::error::handle_error( + function_name, + std::string("IShellLink::SetWorkingDirectory failed: ") + + hr_hex(result)); + psl->Release(); + return false; + } + } + + result = psl->SetShowCmd(SW_SHOWNORMAL); + if (FAILED(result)) { + utils::error::handle_error(function_name, + std::string("IShellLink::SetShowCmd failed: ") + + hr_hex(result)); + psl->Release(); + return false; + } + + if (not cfg.icon_path.empty()) { + result = psl->SetIconLocation(cfg.icon_path.c_str(), 0); + if (FAILED(result)) { + utils::error::handle_error( + function_name, + std::string("IShellLink::SetIconLocation failed: ") + hr_hex(result)); + psl->Release(); + return false; + } + } + + if (not utils::file::file{lnk_path}.remove()) { + utils::error::handle_error(function_name, + "failed to remove existing shortcut|path|" + + utils::string::to_utf8(lnk_path)); + return false; + } + + IPersistFile *ppf{nullptr}; + result = psl->QueryInterface(IID_PPV_ARGS(&ppf)); + if (FAILED(result)) { + utils::error::handle_error( + function_name, + std::string("QueryInterface(IPersistFile) failed: ") + hr_hex(result)); + psl->Release(); + return false; + } + + result = ppf->Save(lnk_path.c_str(), TRUE); + ppf->SaveCompleted(lnk_path.c_str()); + + ppf->Release(); + psl->Release(); + + if (FAILED(result)) { + utils::error::handle_error(function_name, + std::string("IPersistFile::Save failed: ") + + hr_hex(result)); + return false; + } + + return true; +} + +auto remove_shortcut(std::wstring shortcut_name, const std::wstring &location) + -> bool { + if (not shortcut_name.ends_with(L".lnk")) { + shortcut_name += L".lnk"; + } + + auto file = utils::path::combine(location, {shortcut_name}); + if (not utils::file::file{file}.exists()) { + return true; + } + + return utils::file::file{file}.remove(); +} } // namespace monitarr::utils #endif // defined(_WIN32) diff --git a/support/test/src/utils/atomic_test.cpp b/support/test/src/utils/atomic_test.cpp new file mode 100644 index 0000000..78f1bcd --- /dev/null +++ b/support/test/src/utils/atomic_test.cpp @@ -0,0 +1,73 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include "test.hpp" + +namespace { +struct config final { + std::string a; + std::string b; +}; +} // namespace + +namespace monitarr { +TEST(utils_atomic_test, atomic_primitive) { + utils::atomic value; + value = 5U; + EXPECT_EQ(5U, static_cast(value)); + EXPECT_EQ(5U, value.load()); + + value.store(6U); + EXPECT_EQ(6U, static_cast(value)); + EXPECT_EQ(6U, value.load()); +} + +TEST(utils_atomic_test, atomic_primitive_equality) { + utils::atomic value1{5U}; + utils::atomic value2{5U}; + EXPECT_EQ(value1, value1); + EXPECT_EQ(value2, value2); + EXPECT_EQ(value1, value2); + EXPECT_EQ(static_cast(value1), 5U); + EXPECT_EQ(static_cast(value2), 5U); +} + +TEST(utils_atomic_test, atomic_primitive_inequality) { + utils::atomic value1{5U}; + utils::atomic value2{6U}; + EXPECT_NE(value1, value2); + EXPECT_NE(static_cast(value1), 6U); + EXPECT_NE(static_cast(value2), 5U); +} + +TEST(utils_atomic_test, atomic_struct) { + utils::atomic value{ + config{ + .a = "a", + .b = "b", + }, + }; + + auto data = static_cast(value); + EXPECT_STREQ("a", data.a.c_str()); + EXPECT_STREQ("b", data.b.c_str()); +} +} // namespace monitarr diff --git a/support/test/src/utils/base64_test.cpp b/support/test/src/utils/base64_test.cpp new file mode 100644 index 0000000..4a973e9 --- /dev/null +++ b/support/test/src/utils/base64_test.cpp @@ -0,0 +1,389 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include "test.hpp" + +using macaron::Base64::Decode; +using macaron::Base64::Encode; +using macaron::Base64::EncodeUrlSafe; + +namespace { +[[nodiscard]] auto decode_to_string(std::string_view str) -> std::string { + auto vec = Decode(str); + return {vec.begin(), vec.end()}; +} + +[[nodiscard]] auto standard_to_url_safe(std::string str, bool keep_padding) + -> std::string { + for (auto &cur_ch : str) { + if (cur_ch == '+') { + cur_ch = '-'; + } else if (cur_ch == '/') { + cur_ch = '_'; + } + } + if (not keep_padding) { + while (not str.empty() && str.back() == '=') { + str.pop_back(); + } + } + return str; +} +} // namespace + +TEST(utils_base64, rfc4648_known_vectors_standard_padded) { + struct vec_case { + std::string_view in; + std::string_view b64; + }; + const std::array vectors{{ + {"", ""}, + {"f", "Zg=="}, + {"fo", "Zm8="}, + {"foo", "Zm9v"}, + {"foob", "Zm9vYg=="}, + {"fooba", "Zm9vYmE="}, + {"foobar", "Zm9vYmFy"}, + }}; + + for (const auto &vec_entry : vectors) { + const auto enc_str = + Encode(reinterpret_cast(vec_entry.in.data()), + vec_entry.in.size(), /*url_safe=*/false, /*pad=*/true); + EXPECT_EQ(enc_str, vec_entry.b64); + + const auto dec_vec = Decode(vec_entry.b64); + EXPECT_EQ(std::string(dec_vec.begin(), dec_vec.end()), vec_entry.in); + } +} + +TEST(utils_base64, url_safe_padded_and_unpadded_match_transformed_standard) { + const std::string payload = + std::string("This+/needs/URL-safe mapping and padding checks.") + + std::string("\x00\x01\xFE\xFF", 4); + + const auto std_padded = + Encode(reinterpret_cast(payload.data()), + payload.size(), /*url_safe=*/false, /*pad=*/true); + const auto url_padded = + Encode(reinterpret_cast(payload.data()), + payload.size(), /*url_safe=*/true, /*pad=*/true); + const auto url_unpadded = + Encode(reinterpret_cast(payload.data()), + payload.size(), /*url_safe=*/true, /*pad=*/false); + + const auto url_from_std_padded = + standard_to_url_safe(std_padded, /*keep_padding=*/true); + const auto url_from_std_unpadded = + standard_to_url_safe(std_padded, /*keep_padding=*/false); + + EXPECT_EQ(url_padded, url_from_std_padded); + EXPECT_EQ(url_unpadded, url_from_std_unpadded); + + const auto dec_one = Decode(url_padded); + const auto dec_two = Decode(url_unpadded); + EXPECT_EQ(std::string(dec_one.begin(), dec_one.end()), payload); + EXPECT_EQ(std::string(dec_two.begin(), dec_two.end()), payload); +} + +TEST(utils_base64, empty_input) { + const std::string empty_str; + const auto enc_empty_std = + Encode(reinterpret_cast(empty_str.data()), + empty_str.size(), /*url_safe=*/false, /*pad=*/true); + const auto enc_empty_url = + Encode(reinterpret_cast(empty_str.data()), + empty_str.size(), /*url_safe=*/true, /*pad=*/false); + EXPECT_TRUE(enc_empty_std.empty()); + EXPECT_TRUE(enc_empty_url.empty()); + + const auto dec_empty = Decode(""); + EXPECT_TRUE(dec_empty.empty()); +} + +TEST(utils_base64, remainder_boundaries_round_trip) { + const std::string str_one = "A"; // rem 1 + const std::string str_two = "AB"; // rem 2 + const std::string str_thr = "ABC"; // rem 0 + const std::string str_fou = "ABCD"; // rem 1 after blocks + const std::string str_fiv = "ABCDE"; // rem 2 after blocks + + for (const auto *str_ptr : + {&str_one, &str_two, &str_thr, &str_fou, &str_fiv}) { + const auto enc_std = + Encode(reinterpret_cast(str_ptr->data()), + str_ptr->size(), false, true); + const auto dec_std = Decode(enc_std); + EXPECT_EQ(std::string(dec_std.begin(), dec_std.end()), *str_ptr); + + const auto enc_url_pad = + Encode(reinterpret_cast(str_ptr->data()), + str_ptr->size(), true, true); + const auto dec_url_pad = Decode(enc_url_pad); + EXPECT_EQ(std::string(dec_url_pad.begin(), dec_url_pad.end()), *str_ptr); + + const auto enc_url_nopad = + Encode(reinterpret_cast(str_ptr->data()), + str_ptr->size(), true, false); + const auto dec_url_nopad = Decode(enc_url_nopad); + EXPECT_EQ(std::string(dec_url_nopad.begin(), dec_url_nopad.end()), + *str_ptr); + } +} + +TEST(utils_base64, decode_accepts_standard_and_url_safe_forms) { + const std::string input_str = "Man is distinguished, not only by his reason."; + const auto std_padded = + Encode(reinterpret_cast(input_str.data()), + input_str.size(), false, true); + const auto url_padded = + Encode(reinterpret_cast(input_str.data()), + input_str.size(), true, true); + const auto url_unpadded = + Encode(reinterpret_cast(input_str.data()), + input_str.size(), true, false); + + const auto dec_std = Decode(std_padded); + const auto dec_url_pad = Decode(url_padded); + const auto dec_url_nopad = Decode(url_unpadded); + + EXPECT_EQ(std::string(dec_std.begin(), dec_std.end()), input_str); + EXPECT_EQ(std::string(dec_url_pad.begin(), dec_url_pad.end()), input_str); + EXPECT_EQ(std::string(dec_url_nopad.begin(), dec_url_nopad.end()), input_str); +} + +TEST(utils_base64, all_byte_values_round_trip) { + std::vector byte_vec(256); + for (size_t idx = 0; idx < byte_vec.size(); ++idx) { + byte_vec[idx] = static_cast(idx); + } + + const auto enc_std = Encode(byte_vec.data(), byte_vec.size(), false, true); + const auto dec_std = Decode(enc_std); + ASSERT_EQ(dec_std.size(), byte_vec.size()); + EXPECT_TRUE(std::equal(dec_std.begin(), dec_std.end(), byte_vec.begin())); + + const auto enc_url = Encode(byte_vec.data(), byte_vec.size(), true, false); + const auto dec_url = Decode(enc_url); + ASSERT_EQ(dec_url.size(), byte_vec.size()); + EXPECT_TRUE(std::equal(dec_url.begin(), dec_url.end(), byte_vec.begin())); +} + +TEST(utils_base64, wrapper_encode_url_safe_equals_flagged_encode) { + const std::string data_str = "wrap me!"; + const auto enc_wrap_a = + EncodeUrlSafe(reinterpret_cast(data_str.data()), + data_str.size(), /*pad=*/false); + const auto enc_wrap_b = + Encode(reinterpret_cast(data_str.data()), + data_str.size(), /*url_safe=*/true, /*pad=*/false); + EXPECT_EQ(enc_wrap_a, enc_wrap_b); + + const auto enc_wrap_a2 = EncodeUrlSafe(data_str, /*pad=*/true); + const auto enc_wrap_b2 = Encode(data_str, /*url_safe=*/true, /*pad=*/true); + EXPECT_EQ(enc_wrap_a2, enc_wrap_b2); +} + +TEST(utils_base64, unpadded_length_rules) { + const auto enc_one = Encode("f", /*url_safe=*/true, /*pad=*/false); + const auto enc_two = Encode("fo", /*url_safe=*/true, /*pad=*/false); + const auto enc_thr = Encode("foo", /*url_safe=*/true, /*pad=*/false); + EXPECT_EQ(enc_one.size(), 2U); + EXPECT_EQ(enc_two.size(), 3U); + EXPECT_EQ(enc_thr.size(), 4U); + + EXPECT_EQ(Decode(enc_one), std::vector({'f'})); + EXPECT_EQ(Decode(enc_two), std::vector({'f', 'o'})); + EXPECT_EQ(Decode(enc_thr), std::vector({'f', 'o', 'o'})); +} + +TEST(utils_base64, errors_length_mod4_eq_1) { + EXPECT_THROW(Decode("A"), std::runtime_error); + EXPECT_THROW(Decode("AAAAA"), std::runtime_error); +} + +TEST(utils_base64, errors_invalid_characters) { + EXPECT_THROW(Decode("Zm9v YmFy"), std::runtime_error); + EXPECT_THROW(Decode("Zm9v*YmFy"), std::runtime_error); + EXPECT_THROW(Decode("Z=g="), std::runtime_error); +} + +TEST(utils_base64, reject_whitespace_and_controls) { + // newline, tab, and space should be rejected (decoder does not skip + // whitespace) + EXPECT_THROW(Decode("Zg==\n"), std::runtime_error); + EXPECT_THROW(Decode("Zg==\t"), std::runtime_error); + EXPECT_THROW(Decode("Z g=="), std::runtime_error); +} + +TEST(utils_base64, reject_padding_in_nonfinal_quartet) { + // '=' cannot appear before the final quartet + EXPECT_THROW(Decode("AAA=AAAA"), std::runtime_error); + EXPECT_THROW(Decode("Zg==Zg=="), std::runtime_error); +} + +TEST(utils_base64, reject_padding_in_first_two_slots_of_final_quartet) { + // '=' only allowed in slots 3 and/or 4 of the final quartet + EXPECT_THROW(Decode("=AAA"), std::runtime_error); + EXPECT_THROW(Decode("A=AA"), std::runtime_error); + EXPECT_THROW( + Decode("Z=g="), + std::runtime_error); // already in your suite, kept for completeness +} + +TEST(utils_base64, reject_incorrect_padding_count_for_length) { + // "f" must be "Zg==" (two '='). One '=' is invalid. + EXPECT_THROW(Decode("Zg="), std::runtime_error); + + // "foo" must be unpadded ("Zm9v"). Extra '=' is invalid. + EXPECT_THROW(Decode("Zm9v="), std::runtime_error); + + // "fo" must have exactly one '=' -> "Zm8=" + // Correct cases: + EXPECT_NO_THROW(Decode("Zm8=")); + EXPECT_NO_THROW(Decode("Zm9v")); +} + +TEST(utils_base64, accept_unpadded_equivalents_when_legal) { + EXPECT_EQ(decode_to_string("Zg"), "f"); + EXPECT_EQ(decode_to_string("Zm8"), "fo"); + EXPECT_EQ(decode_to_string("Zm9v"), "foo"); + EXPECT_EQ(decode_to_string("Zm9vYmE"), "fooba"); +} + +TEST(utils_base64, mixed_alphabet_is_accepted) { + const std::string input_str = "any+/mix_/of+chars/"; + const auto std_padded = + Encode(reinterpret_cast(input_str.data()), + input_str.size(), /*url_safe=*/false, /*pad=*/true); + + std::string mixed = std_padded; + for (char &cur_ch : mixed) { + if (cur_ch == '+') { + cur_ch = '-'; + } else if (cur_ch == '/') { + cur_ch = '_'; + } + } + + EXPECT_EQ(decode_to_string(mixed), input_str); +} + +TEST(utils_base64, invalid_non_ascii_octets_in_input) { + // Extended bytes like 0xFF are not valid Base64 characters + std::string bad = "Zg=="; + bad[1] = static_cast(0xFF); + EXPECT_THROW(Decode(bad), std::runtime_error); +} + +TEST(utils_base64, large_buffer_round_trip_and_sizes) { + // Deterministic pseudo-random buffer + const std::size_t byte_len = 1 << 20; // 1 MiB + std::vector data_vec(byte_len); + unsigned int val = 0x12345678U; + for (unsigned char &idx : data_vec) { + val ^= val << 13; + val ^= val >> 17; + val ^= val << 5; // xorshift32 + idx = static_cast(val & 0xFFU); + } + + // Padded encode length should be 4 * ceil(N/3) + const auto enc_pad = Encode(data_vec.data(), data_vec.size(), + /*url_safe=*/false, /*pad=*/true); + const std::size_t expected_padded = 4U * ((byte_len + 2U) / 3U); + EXPECT_EQ(enc_pad.size(), expected_padded); + + // Unpadded encode length rule (RFC 4648 §5) + const auto enc_nopad = Encode(data_vec.data(), data_vec.size(), + /*url_safe=*/true, /*pad=*/false); + const std::size_t rem = byte_len % 3U; + const std::size_t expected_unpadded = + 4U * (byte_len / 3U) + (rem == 0U ? 0U : (rem == 1U ? 2U : 3U)); + EXPECT_EQ(enc_nopad.size(), expected_unpadded); + + // Round-trips + const auto dec_pad = Decode(enc_pad); + const auto dec_nopad = Decode(enc_nopad); + ASSERT_EQ(dec_pad.size(), data_vec.size()); + ASSERT_EQ(dec_nopad.size(), data_vec.size()); + EXPECT_TRUE(std::equal(dec_pad.begin(), dec_pad.end(), data_vec.begin())); + EXPECT_TRUE(std::equal(dec_nopad.begin(), dec_nopad.end(), data_vec.begin())); +} + +TEST(utils_base64, url_safe_round_trip_various_lengths) { + for (std::size_t len : {0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 32U, 33U, 64U, 65U}) { + std::vector buf(len); + for (std::size_t i = 0; i < len; ++i) { + buf[i] = static_cast(i * 13U + 7U); + } + + const auto enc_unpadded = + Encode(buf.data(), buf.size(), /*url_safe=*/true, /*pad=*/false); + const auto enc_padded = + Encode(buf.data(), buf.size(), /*url_safe=*/true, /*pad=*/true); + + const auto dec_unpadded = Decode(enc_unpadded); + const auto dec_padded = Decode(enc_padded); + + ASSERT_EQ(dec_unpadded.size(), buf.size()); + ASSERT_EQ(dec_padded.size(), buf.size()); + EXPECT_TRUE( + std::equal(dec_unpadded.begin(), dec_unpadded.end(), buf.begin())); + EXPECT_TRUE(std::equal(dec_padded.begin(), dec_padded.end(), buf.begin())); + } +} + +TEST(utils_base64, reject_trailing_garbage_after_padding) { + // Anything after final '=' padding is invalid + EXPECT_THROW(Decode("Zg==A"), std::runtime_error); + EXPECT_THROW(Decode("Zm8=A"), std::runtime_error); +} + +TEST(utils_base64, reject_three_padding_chars_total) { + // Any string with total length %4==1 is invalid (e.g., "Zg===") + EXPECT_THROW(Decode("Zg==="), std::runtime_error); +} + +TEST(utils_base64, standard_vs_url_safe_encoding_equivalence) { + const std::string msg = "base64 / url-safe + cross-check"; + + const auto std_enc = + Encode(reinterpret_cast(msg.data()), msg.size(), + /*url_safe=*/false, /*pad=*/true); + const auto url_enc = + Encode(reinterpret_cast(msg.data()), msg.size(), + /*url_safe=*/true, /*pad=*/true); + + std::string transformed = std_enc; + for (char &cur_ch : transformed) { + if (cur_ch == '+') { + cur_ch = '-'; + } else if (cur_ch == '/') { + cur_ch = '_'; + } + } + + EXPECT_EQ(url_enc, transformed); + + // decode once, then construct + EXPECT_EQ(decode_to_string(url_enc), msg); +} diff --git a/support/test/src/utils/encrypting_reader_test.cpp b/support/test/src/utils/encrypting_reader_test.cpp index d77ad9c..11c6206 100644 --- a/support/test/src/utils/encrypting_reader_test.cpp +++ b/support/test/src/utils/encrypting_reader_test.cpp @@ -35,7 +35,8 @@ TEST(utils_encrypting_reader, read_file_data) { EXPECT_TRUE(source_file); if (source_file) { utils::encryption::encrypting_reader reader( - "test.dat", source_file.get_path(), get_stop_requested, token); + "test.dat", source_file.get_path(), get_stop_requested, token, + std::nullopt); for (std::uint8_t i = 0U; i < 8U; i++) { data_buffer buffer( @@ -67,6 +68,109 @@ TEST(utils_encrypting_reader, read_file_data) { } } +TEST(utils_encrypting_reader, read_file_data_using_argon2id) { + const auto token = std::string("moose"); + utils::encryption::kdf_config cfg; + + auto &source_file = test::create_random_file( + 8U * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, token, cfg, + std::nullopt); + + std::array hdr; + EXPECT_EQ(hdr.size(), utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(hdr.data()), hdr.size(), + 1U, &reader)); + EXPECT_TRUE(utils::encryption::kdf_config::from_header(hdr, cfg)); + + for (std::uint8_t i = 0U; i < 8U; i++) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size()); + for (std::uint8_t j = 0U; j < 2U; j++) { + ASSERT_EQ( + buffer.size() / 2U, + utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(&buffer[(buffer.size() / 2U) * j]), + buffer.size() / 2U, 1U, &reader)); + } + + data_buffer decrypted_data; + EXPECT_TRUE(utils::encryption::decrypt_data( + token, *reader.get_kdf_config_for_data(), buffer, decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + utils::encryption::encrypting_reader::get_data_chunk_size() * i, + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } +} + +TEST(utils_encrypting_reader, read_file_data_using_argon2id_master_key) { + const auto token = std::string("moose"); + utils::encryption::kdf_config cfg; + + auto master_key = + utils::encryption::generate_key(token, cfg); + + auto &source_file = test::create_random_file( + 8U * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, master_key, cfg, + std::nullopt); + + std::array hdr; + EXPECT_EQ(hdr.size(), utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(hdr.data()), hdr.size(), + 1U, &reader)); + EXPECT_TRUE(utils::encryption::kdf_config::from_header(hdr, cfg)); + + for (std::uint8_t i = 0U; i < 8U; i++) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size()); + for (std::uint8_t j = 0U; j < 2U; j++) { + ASSERT_EQ( + buffer.size() / 2U, + utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(&buffer[(buffer.size() / 2U) * j]), + buffer.size() / 2U, 1U, &reader)); + } + + auto data_cfg = *reader.get_kdf_config_for_data(); + utils::hash::hash_256_t data_key; + std::tie(data_key, std::ignore) = cfg.create_subkey( + utils::encryption::kdf_context::data, data_cfg.unique_id, master_key); + data_buffer decrypted_data; + EXPECT_TRUE( + utils::encryption::decrypt_data(data_key, buffer, decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + utils::encryption::encrypting_reader::get_data_chunk_size() * i, + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } +} + TEST(utils_encrypting_reader, read_file_data_in_multiple_chunks) { const auto token = std::string("moose"); auto &source_file = test::create_random_file( @@ -74,7 +178,8 @@ TEST(utils_encrypting_reader, read_file_data_in_multiple_chunks) { EXPECT_TRUE(source_file); if (source_file) { utils::encryption::encrypting_reader reader( - "test.dat", source_file.get_path(), get_stop_requested, token); + "test.dat", source_file.get_path(), get_stop_requested, token, + std::nullopt); for (std::uint8_t i = 0U; i < 8U; i += 2U) { data_buffer buffer( @@ -114,6 +219,128 @@ TEST(utils_encrypting_reader, read_file_data_in_multiple_chunks) { } } +TEST(utils_encrypting_reader, + read_file_data_in_multiple_chunks_using_argon2id) { + utils::encryption::kdf_config cfg; + + const auto token = std::string("moose"); + auto &source_file = test::create_random_file( + 8U * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, token, cfg, + std::nullopt); + + std::array hdr; + EXPECT_EQ(hdr.size(), utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(hdr.data()), hdr.size(), + 1U, &reader)); + EXPECT_TRUE(utils::encryption::kdf_config::from_header(hdr, cfg)); + + for (std::uint8_t i = 0U; i < 8U; i += 2U) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size() * + 2U); + EXPECT_EQ(buffer.size(), + utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(buffer.data()), buffer.size(), 1U, + &reader)); + + for (std::uint8_t j = 0U; j < 2U; j++) { + data_buffer decrypted_data; + const auto offset = (j * (buffer.size() / 2U)); + EXPECT_TRUE(utils::encryption::decrypt_data( + token, *reader.get_kdf_config_for_data(), + data_buffer( + std::next(buffer.begin(), static_cast(offset)), + std::next(buffer.begin(), static_cast( + offset + (buffer.size() / 2U)))), + decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + (utils::encryption::encrypting_reader::get_data_chunk_size() * i) + + (j * + utils::encryption::encrypting_reader::get_data_chunk_size()), + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } + } +} + +TEST(utils_encrypting_reader, + read_file_data_in_multiple_chunks_using_argon2id_master_key) { + const auto token = std::string("moose"); + utils::encryption::kdf_config cfg; + + auto master_key = + utils::encryption::generate_key(token, cfg); + + auto &source_file = test::create_random_file( + 8U * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, master_key, cfg, + std::nullopt); + + std::array hdr; + EXPECT_EQ(hdr.size(), utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(hdr.data()), hdr.size(), + 1U, &reader)); + EXPECT_TRUE(utils::encryption::kdf_config::from_header(hdr, cfg)); + + for (std::uint8_t i = 0U; i < 8U; i += 2U) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size() * + 2U); + EXPECT_EQ(buffer.size(), + utils::encryption::encrypting_reader::reader_function( + reinterpret_cast(buffer.data()), buffer.size(), 1U, + &reader)); + + auto data_cfg = *reader.get_kdf_config_for_data(); + utils::hash::hash_256_t data_key; + std::tie(data_key, std::ignore) = cfg.create_subkey( + utils::encryption::kdf_context::data, data_cfg.unique_id, master_key); + + for (std::uint8_t j = 0U; j < 2U; j++) { + data_buffer decrypted_data; + const auto offset = (j * (buffer.size() / 2U)); + EXPECT_TRUE(utils::encryption::decrypt_data( + data_key, + data_buffer( + std::next(buffer.begin(), static_cast(offset)), + std::next(buffer.begin(), static_cast( + offset + (buffer.size() / 2U)))), + decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + (utils::encryption::encrypting_reader::get_data_chunk_size() * i) + + (j * + utils::encryption::encrypting_reader::get_data_chunk_size()), + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } + } +} + TEST(utils_encrypting_reader, read_file_data_as_stream) { const auto token = std::string("moose"); auto &source_file = test::create_random_file( @@ -121,7 +348,8 @@ TEST(utils_encrypting_reader, read_file_data_as_stream) { EXPECT_TRUE(source_file); if (source_file) { utils::encryption::encrypting_reader reader( - "test.dat", source_file.get_path(), get_stop_requested, token); + "test.dat", source_file.get_path(), get_stop_requested, token, + std::nullopt); auto io_stream = reader.create_iostream(); EXPECT_FALSE(io_stream->seekg(0, std::ios_base::end).fail()); EXPECT_TRUE(io_stream->good()); @@ -166,6 +394,129 @@ TEST(utils_encrypting_reader, read_file_data_as_stream) { } } +TEST(utils_encrypting_reader, read_file_data_as_stream_using_argon2id) { + utils::encryption::kdf_config cfg; + + const auto token = std::string("moose"); + auto &source_file = test::create_random_file( + 8U * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, token, cfg, + std::nullopt); + auto io_stream = reader.create_iostream(); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::end).fail()); + EXPECT_TRUE(io_stream->good()); + EXPECT_EQ(reader.get_total_size(), + static_cast(io_stream->tellg())); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::beg).fail()); + EXPECT_TRUE(io_stream->good()); + + for (std::uint8_t i = 0U; i < 8U; i++) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size()); + EXPECT_FALSE( + io_stream + ->seekg(static_cast( + i * buffer.size() + utils::encryption::kdf_config::size())) + .fail()); + EXPECT_TRUE(io_stream->good()); + for (std::uint8_t j = 0U; j < 2U; j++) { + EXPECT_FALSE( + io_stream + ->read( + reinterpret_cast(&buffer[(buffer.size() / 2U) * j]), + static_cast(buffer.size()) / 2U) + .fail()); + EXPECT_TRUE(io_stream->good()); + } + + data_buffer decrypted_data; + EXPECT_TRUE(utils::encryption::decrypt_data( + token, *reader.get_kdf_config_for_data(), buffer, decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + utils::encryption::encrypting_reader::get_data_chunk_size() * i, + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } +} + +TEST(utils_encrypting_reader, + read_file_data_as_stream_using_argon2id_master_key) { + const auto token = std::string("moose"); + utils::encryption::kdf_config cfg; + + auto master_key = + utils::encryption::generate_key(token, cfg); + + auto &source_file = test::create_random_file( + 8U * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, master_key, cfg, + std::nullopt); + auto io_stream = reader.create_iostream(); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::end).fail()); + EXPECT_TRUE(io_stream->good()); + EXPECT_EQ(reader.get_total_size(), + static_cast(io_stream->tellg())); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::beg).fail()); + EXPECT_TRUE(io_stream->good()); + + for (std::uint8_t i = 0U; i < 8U; i++) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size()); + EXPECT_FALSE( + io_stream + ->seekg(static_cast( + i * buffer.size() + utils::encryption::kdf_config::size())) + .fail()); + EXPECT_TRUE(io_stream->good()); + for (std::uint8_t j = 0U; j < 2U; j++) { + EXPECT_FALSE( + io_stream + ->read( + reinterpret_cast(&buffer[(buffer.size() / 2U) * j]), + static_cast(buffer.size()) / 2U) + .fail()); + EXPECT_TRUE(io_stream->good()); + } + + auto data_cfg = *reader.get_kdf_config_for_data(); + utils::hash::hash_256_t data_key; + std::tie(data_key, std::ignore) = cfg.create_subkey( + utils::encryption::kdf_context::data, data_cfg.unique_id, master_key); + + data_buffer decrypted_data; + EXPECT_TRUE( + utils::encryption::decrypt_data(data_key, buffer, decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + utils::encryption::encrypting_reader::get_data_chunk_size() * i, + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } +} + TEST(utils_encrypting_reader, read_file_data_in_multiple_chunks_as_stream) { const auto token = std::string("moose"); auto &source_file = test::create_random_file( @@ -173,7 +524,8 @@ TEST(utils_encrypting_reader, read_file_data_in_multiple_chunks_as_stream) { EXPECT_TRUE(source_file); if (source_file) { utils::encryption::encrypting_reader reader( - "test.dat", source_file.get_path(), get_stop_requested, token); + "test.dat", source_file.get_path(), get_stop_requested, token, + std::nullopt); auto io_stream = reader.create_iostream(); EXPECT_FALSE(io_stream->seekg(0, std::ios_base::end).fail()); EXPECT_TRUE(io_stream->good()); @@ -220,6 +572,142 @@ TEST(utils_encrypting_reader, read_file_data_in_multiple_chunks_as_stream) { } } } + +TEST(utils_encrypting_reader, + read_file_data_in_multiple_chunks_as_stream_using_argon2id) { + utils::encryption::kdf_config cfg; + + const auto token = std::string("moose"); + auto &source_file = test::create_random_file( + 8u * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, token, cfg, + std::nullopt); + auto io_stream = reader.create_iostream(); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::end).fail()); + EXPECT_TRUE(io_stream->good()); + EXPECT_EQ(reader.get_total_size(), + static_cast(io_stream->tellg())); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::beg).fail()); + EXPECT_TRUE(io_stream->good()); + + EXPECT_FALSE(io_stream + ->seekg(static_cast( + utils::encryption::kdf_config::size())) + .fail()); + + for (std::uint8_t i = 0U; i < 8U; i += 2U) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size() * + 2U); + EXPECT_FALSE(io_stream + ->read(reinterpret_cast(buffer.data()), + static_cast(buffer.size())) + .fail()); + EXPECT_TRUE(io_stream->good()); + + for (std::uint8_t j = 0U; j < 2U; j++) { + data_buffer decrypted_data; + const auto offset = (j * (buffer.size() / 2U)); + EXPECT_TRUE(utils::encryption::decrypt_data( + token, *reader.get_kdf_config_for_data(), + data_buffer( + std::next(buffer.begin(), static_cast(offset)), + std::next(buffer.begin(), static_cast( + offset + (buffer.size() / 2U)))), + decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + (utils::encryption::encrypting_reader::get_data_chunk_size() * i) + + (j * + utils::encryption::encrypting_reader::get_data_chunk_size()), + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } + } +} + +TEST(utils_encrypting_reader, + read_file_data_in_multiple_chunks_as_stream_using_argon2id_master_key) { + const auto token = std::string("moose"); + utils::encryption::kdf_config cfg; + + auto master_key = + utils::encryption::generate_key(token, cfg); + + auto &source_file = test::create_random_file( + 8u * utils::encryption::encrypting_reader::get_data_chunk_size()); + EXPECT_TRUE(source_file); + if (source_file) { + utils::encryption::encrypting_reader reader( + "test.dat", source_file.get_path(), get_stop_requested, master_key, cfg, + std::nullopt); + auto io_stream = reader.create_iostream(); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::end).fail()); + EXPECT_TRUE(io_stream->good()); + EXPECT_EQ(reader.get_total_size(), + static_cast(io_stream->tellg())); + EXPECT_FALSE(io_stream->seekg(0, std::ios_base::beg).fail()); + EXPECT_TRUE(io_stream->good()); + + EXPECT_FALSE(io_stream + ->seekg(static_cast( + utils::encryption::kdf_config::size())) + .fail()); + + for (std::uint8_t i = 0U; i < 8U; i += 2U) { + data_buffer buffer( + utils::encryption::encrypting_reader::get_encrypted_chunk_size() * + 2U); + EXPECT_FALSE(io_stream + ->read(reinterpret_cast(buffer.data()), + static_cast(buffer.size())) + .fail()); + EXPECT_TRUE(io_stream->good()); + + auto data_cfg = *reader.get_kdf_config_for_data(); + utils::hash::hash_256_t data_key; + std::tie(data_key, std::ignore) = cfg.create_subkey( + utils::encryption::kdf_context::data, data_cfg.unique_id, master_key); + + for (std::uint8_t j = 0U; j < 2U; j++) { + data_buffer decrypted_data; + const auto offset = (j * (buffer.size() / 2U)); + EXPECT_TRUE(utils::encryption::decrypt_data( + data_key, + data_buffer( + std::next(buffer.begin(), static_cast(offset)), + std::next(buffer.begin(), static_cast( + offset + (buffer.size() / 2U)))), + decrypted_data)); + + EXPECT_EQ(utils::encryption::encrypting_reader::get_data_chunk_size(), + decrypted_data.size()); + + std::size_t bytes_read{}; + data_buffer file_data(decrypted_data.size()); + EXPECT_TRUE(source_file.read( + file_data, + (utils::encryption::encrypting_reader::get_data_chunk_size() * i) + + (j * + utils::encryption::encrypting_reader::get_data_chunk_size()), + &bytes_read)); + EXPECT_EQ(0, std::memcmp(file_data.data(), decrypted_data.data(), + file_data.size())); + } + } + } +} } // namespace monitarr #endif // defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST) diff --git a/support/test/src/utils/encryption_kdf_config_test.cpp b/support/test/src/utils/encryption_kdf_config_test.cpp new file mode 100644 index 0000000..74e1136 --- /dev/null +++ b/support/test/src/utils/encryption_kdf_config_test.cpp @@ -0,0 +1,402 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include "test.hpp" + +#if defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST) + +namespace monitarr { +TEST(utils_encryption_kdf_config, can_construct_using_default_constructor) { + utils::encryption::kdf_config cfg; + + EXPECT_EQ(utils::encryption::kdf_version::v1, cfg.version); + EXPECT_EQ(utils::encryption::kdf_type::argon2id, cfg.kdf); + EXPECT_EQ(utils::encryption::memlimit_level::level3, cfg.memlimit); + EXPECT_EQ(utils::encryption::opslimit_level::level2, cfg.opslimit); + EXPECT_EQ(utils::encryption::kdf_config::salt_t{}, cfg.salt); + EXPECT_EQ(0U, cfg.unique_id); + EXPECT_EQ(0U, cfg.checksum); +} + +TEST(utils_encryption_kdf_config, can_seal) { + utils::encryption::kdf_config cfg; + cfg.seal(); + EXPECT_NE(utils::encryption::kdf_config::salt_t{}, cfg.salt); + + auto orig_salt = cfg.salt; + cfg.seal(); + EXPECT_NE(orig_salt, cfg.salt); +} + +TEST(utils_encryption_kdf_config, can_generate_checksum) { + utils::encryption::kdf_config cfg; + EXPECT_EQ(13087047540462255120ULL, cfg.generate_checksum()); +} + +TEST(utils_encryption_kdf_config, seal_calculates_checksum) { + utils::encryption::kdf_config cfg; + cfg.seal(); + + EXPECT_NE(0U, cfg.checksum); +} + +TEST(utils_encryption_kdf_config, can_create_header_and_restore) { + utils::encryption::kdf_config cfg; + cfg.unique_id = 2U; + cfg.seal(); + auto hdr = cfg.to_header(); + + EXPECT_EQ(utils::encryption::kdf_config::size(), hdr.size()); + + utils::encryption::kdf_config restored_cfg; + EXPECT_TRUE(utils::encryption::kdf_config::from_header(hdr, restored_cfg)); + auto restored_hdr = restored_cfg.to_header(); + + EXPECT_EQ(hdr, restored_hdr); + EXPECT_EQ(cfg.version, restored_cfg.version); + EXPECT_EQ(cfg.kdf, restored_cfg.kdf); + EXPECT_EQ(cfg.memlimit, restored_cfg.memlimit); + EXPECT_EQ(cfg.opslimit, restored_cfg.opslimit); + EXPECT_EQ(cfg.salt, restored_cfg.salt); + EXPECT_EQ(cfg.checksum, restored_cfg.checksum); + EXPECT_EQ(cfg.unique_id, restored_cfg.unique_id); + EXPECT_EQ(cfg, restored_cfg); +} + +TEST(utils_encryption_kdf_config, header_restore_fails_if_version_is_invalid) { + utils::encryption::kdf_config cfg; + cfg.version = static_cast(0x11); + cfg.seal(); + + auto hdr = cfg.to_header(); + utils::encryption::kdf_config restored_cfg; + EXPECT_FALSE(utils::encryption::kdf_config::from_header(hdr, restored_cfg)); +} + +TEST(utils_encryption_kdf_config, header_restore_fails_if_kdf_is_invalid) { + utils::encryption::kdf_config cfg; + cfg.kdf = static_cast(0x11); + cfg.seal(); + + auto hdr = cfg.to_header(); + utils::encryption::kdf_config restored_cfg; + EXPECT_FALSE(utils::encryption::kdf_config::from_header(hdr, restored_cfg)); +} + +TEST(utils_encryption_kdf_config, header_restore_fails_if_memlimit_is_invalid) { + utils::encryption::kdf_config cfg; + cfg.memlimit = static_cast(0x11); + cfg.seal(); + + auto hdr = cfg.to_header(); + utils::encryption::kdf_config restored_cfg; + EXPECT_FALSE(utils::encryption::kdf_config::from_header(hdr, restored_cfg)); +} + +TEST(utils_encryption_kdf_config, header_restore_fails_if_opslimit_is_invalid) { + utils::encryption::kdf_config cfg; + cfg.opslimit = static_cast(0x11); + cfg.seal(); + + auto hdr = cfg.to_header(); + utils::encryption::kdf_config restored_cfg; + EXPECT_FALSE(utils::encryption::kdf_config::from_header(hdr, restored_cfg)); +} + +TEST(utils_encryption_kdf_config, header_restore_fails_if_salt_is_invalid) { + utils::encryption::kdf_config cfg; + cfg.seal(); + cfg.salt = utils::encryption::kdf_config::salt_t{}; + + auto hdr = cfg.to_header(); + utils::encryption::kdf_config restored_cfg; + EXPECT_FALSE(utils::encryption::kdf_config::from_header(hdr, restored_cfg)); +} + +TEST(utils_encryption_kdf_config, header_restore_fails_if_id_is_invalid) { + utils::encryption::kdf_config cfg; + cfg.seal(); + cfg.unique_id = 22U; + + auto hdr = cfg.to_header(); + utils::encryption::kdf_config restored_cfg; + EXPECT_FALSE(utils::encryption::kdf_config::from_header(hdr, restored_cfg)); +} + +TEST(utils_encryption_kdf_config, create_subkey_sets_id_and_updates_checksum) { + using hash_t = utils::hash::hash_256_t; + + utils::encryption::kdf_config cfg; + cfg.seal(); + + hash_t master_key = + utils::encryption::generate_key("root-master-key"); + + constexpr std::size_t sub_id = 42; + auto [subkey, out_cfg] = cfg.create_subkey( + utils::encryption::kdf_context::path, sub_id, master_key); + + EXPECT_NE(subkey, hash_t{}); + EXPECT_NE(subkey, master_key); + + EXPECT_EQ(out_cfg.unique_id, static_cast(sub_id)); + EXPECT_EQ(out_cfg.checksum, out_cfg.generate_checksum()); + + EXPECT_EQ(out_cfg.version, cfg.version); + EXPECT_EQ(out_cfg.kdf, cfg.kdf); + EXPECT_EQ(out_cfg.memlimit, cfg.memlimit); + EXPECT_EQ(out_cfg.opslimit, cfg.opslimit); + EXPECT_EQ(out_cfg.salt, cfg.salt); +} + +TEST(utils_encryption_kdf_config, + create_subkey_is_deterministic_for_same_inputs) { + using hash_t = utils::hash::hash_256_t; + + utils::encryption::kdf_config cfg; + cfg.seal(); + + hash_t master_key = + utils::encryption::generate_key("root-master-key"); + + constexpr auto ctx = utils::encryption::kdf_context::data; + constexpr std::size_t sub_id = 7; + + auto [k1, c1] = cfg.create_subkey(ctx, sub_id, master_key); + auto [k2, c2] = cfg.create_subkey(ctx, sub_id, master_key); + + EXPECT_EQ(k1, k2); + EXPECT_EQ(c1.unique_id, c2.unique_id); + EXPECT_EQ(c1.checksum, c2.checksum); + EXPECT_EQ(c1, c2); +} + +TEST(utils_encryption_kdf_config, create_subkey_varies_with_different_id) { + using hash_t = utils::hash::hash_256_t; + + utils::encryption::kdf_config cfg; + cfg.seal(); + + hash_t master_key = + utils::encryption::generate_key("root-master-key"); + + constexpr auto ctx = utils::encryption::kdf_context::data; + + auto [k1, c1] = cfg.create_subkey(ctx, 1, master_key); + auto [k2, c2] = cfg.create_subkey(ctx, 2, master_key); + + EXPECT_NE(k1, k2); + EXPECT_NE(c1.unique_id, c2.unique_id); + EXPECT_NE(c1.checksum, c2.checksum); + + EXPECT_EQ(c1.version, c2.version); + EXPECT_EQ(c1.kdf, c2.kdf); + EXPECT_EQ(c1.memlimit, c2.memlimit); + EXPECT_EQ(c1.opslimit, c2.opslimit); + EXPECT_EQ(c1.salt, c2.salt); +} + +TEST(utils_encryption_kdf_config, create_subkey_varies_with_different_context) { + using hash_t = utils::hash::hash_256_t; + + utils::encryption::kdf_config cfg; + cfg.seal(); + + hash_t master_key = + utils::encryption::generate_key("root-master-key"); + + constexpr std::size_t sub_id = 123; + + auto [ka, ca] = cfg.create_subkey( + utils::encryption::kdf_context::data, sub_id, master_key); + auto [kb, cb] = cfg.create_subkey( + utils::encryption::kdf_context::path, sub_id, master_key); + + EXPECT_NE(ka, kb); + EXPECT_EQ(ca.unique_id, cb.unique_id); + EXPECT_EQ(ca.checksum, cb.checksum); + EXPECT_EQ(ca, cb); +} + +TEST(utils_encryption_kdf_config, + create_subkey_with_undefined_context_uses_fallback) { + using hash_t = utils::hash::hash_256_t; + + utils::encryption::kdf_config cfg; + cfg.seal(); + + hash_t master_key = + utils::encryption::generate_key("root-master-key"); + + constexpr std::size_t sub_id = 55; + + auto [k_def, c_def] = cfg.create_subkey( + utils::encryption::kdf_context::undefined, sub_id, master_key); + auto [k_dat, c_dat] = cfg.create_subkey( + utils::encryption::kdf_context::data, sub_id, master_key); + + EXPECT_NE(k_def, hash_t{}); + EXPECT_NE(k_dat, hash_t{}); + EXPECT_NE(k_def, k_dat); + + EXPECT_EQ(c_def, c_dat); +} + +#if defined(PROJECT_ENABLE_JSON) +TEST(utils_encryption_kdf_config, can_convert_kdf_config_to_and_from_json) { + utils::encryption::kdf_config cfg; + cfg.unique_id = 2U; + cfg.seal(); + + nlohmann::json json_kdf(cfg); + + auto cfg2 = json_kdf.get(); + EXPECT_EQ(cfg, cfg2); +} +#endif // defined(PROJECT_ENABLE_JSON) + +TEST(utils_encryption_kdf_config, equality) { + { + utils::encryption::kdf_config cfg; + utils::encryption::kdf_config cfg2; + + EXPECT_EQ(cfg, cfg2); + } + + { + utils::encryption::kdf_config cfg; + utils::encryption::kdf_config cfg2{cfg}; + + EXPECT_EQ(cfg, cfg2); + } + + { + utils::encryption::kdf_config cfg; + cfg.seal(); + + utils::encryption::kdf_config cfg2{cfg}; + + EXPECT_EQ(cfg, cfg2); + } + + { + utils::encryption::kdf_config cfg; + utils::encryption::kdf_config cfg2; + cfg2 = cfg; + + EXPECT_EQ(cfg, cfg2); + } + + { + utils::encryption::kdf_config cfg; + cfg.seal(); + + utils::encryption::kdf_config cfg2; + cfg2 = cfg; + + EXPECT_EQ(cfg, cfg2); + } +} + +TEST(utils_encryption_kdf_config, sealed_is_not_equal_to_unsealed) { + utils::encryption::kdf_config cfg; + cfg.seal(); + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, sealed_is_not_equal_to_sealed) { + utils::encryption::kdf_config cfg; + cfg.seal(); + + utils::encryption::kdf_config cfg2; + cfg2.seal(); + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, is_not_equal_to_different_id) { + utils::encryption::kdf_config cfg; + cfg.unique_id = 2UL; + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, is_not_equal_to_different_version) { + utils::encryption::kdf_config cfg; + cfg.version = static_cast(0x11); + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, is_not_equal_to_different_kdf) { + utils::encryption::kdf_config cfg; + cfg.kdf = static_cast(0x11); + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, is_not_equal_to_different_memlimit) { + utils::encryption::kdf_config cfg; + cfg.memlimit = static_cast(0x11); + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, is_not_equal_to_different_opslimit) { + utils::encryption::kdf_config cfg; + cfg.opslimit = static_cast(0x11); + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, is_not_equal_to_different_salt) { + utils::encryption::kdf_config cfg; + cfg.salt[0U] = 1U; + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} + +TEST(utils_encryption_kdf_config, is_not_equal_to_different_checksum) { + utils::encryption::kdf_config cfg; + cfg.checksum = 2U; + + utils::encryption::kdf_config cfg2; + + EXPECT_NE(cfg, cfg2); +} +} // namespace monitarr + +#endif // defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST) diff --git a/support/test/src/utils/encryption_read_encrypted_range_test.cpp b/support/test/src/utils/encryption_read_encrypted_range_test.cpp new file mode 100644 index 0000000..f9cb5a3 --- /dev/null +++ b/support/test/src/utils/encryption_read_encrypted_range_test.cpp @@ -0,0 +1,450 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include "test.hpp" + +#if defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST) + +namespace { +[[nodiscard]] auto make_random_plain(std::size_t size) + -> std::vector { + std::vector ret; + ret.resize(size); + + constexpr std::size_t chunk_size = 4096U; + using buf_t = std::array; + + std::size_t written = 0U; + while (written < size) { + auto block = monitarr::utils::generate_secure_random(); + auto to_copy = std::min(chunk_size, size - written); + std::memcpy(ret.data() + written, block.data(), to_copy); + written += to_copy; + } + + return ret; +} + +[[nodiscard]] auto +build_encrypted_blob(const std::vector &plain, + const monitarr::utils::hash::hash_256_t &key, + bool with_kdf, + monitarr::utils::encryption::kdf_config &kdf) + -> std::pair { + monitarr::data_buffer blob; + + if (with_kdf) { + auto hdr = kdf.to_header(); + blob.insert(blob.end(), hdr.begin(), hdr.end()); + } + + auto data_chunk = + monitarr::utils::encryption::encrypting_reader::get_data_chunk_size(); + std::size_t offset = 0U; + + while (offset < plain.size()) { + auto take = std::min(data_chunk, plain.size() - offset); + monitarr::data_buffer buffer; + monitarr::utils::encryption::encrypt_data(key, plain.data() + offset, take, + buffer); + blob.insert(blob.end(), buffer.begin(), buffer.end()); + offset += take; + } + + return {std::move(blob), static_cast(plain.size())}; +} + +[[nodiscard]] auto make_reader(const monitarr::data_buffer &cipher_blob) + -> monitarr::utils::encryption::reader_func_t { + return [&cipher_blob](monitarr::data_buffer &out, std::uint64_t start, + std::uint64_t end) -> bool { + if (end < start) { + return false; + } + + if (end >= static_cast(cipher_blob.size())) { + return false; + } + + auto len = static_cast(end - start + 1U); + out.assign( + std::next(cipher_blob.begin(), static_cast(start)), + std::next(cipher_blob.begin(), + static_cast(start + len))); + return true; + }; +} +} // namespace + +namespace monitarr { +class utils_encryption_read_encrypted_range_fixture + : public ::testing::Test, + public ::testing::WithParamInterface { +protected: + bool uses_kdf{}; + utils::hash::hash_256_t key{}; + utils::encryption::kdf_config kdf{}; + std::size_t chunk{}; + std::size_t plain_sz{}; + std::vector plain; + data_buffer cipher_blob; + std::uint64_t total_size{}; + utils::encryption::reader_func_t reader; + + void SetUp() override { + uses_kdf = GetParam(); + + key = + uses_kdf + ? utils::encryption::generate_key("moose", + kdf) + : utils::encryption::generate_key("moose"); + chunk = utils::encryption::encrypting_reader::get_data_chunk_size(); + + plain_sz = (2U * chunk) + (chunk / 2U); + + plain = make_random_plain(plain_sz); + std::tie(cipher_blob, total_size) = + build_encrypted_blob(plain, key, uses_kdf, kdf); + reader = make_reader(cipher_blob); + } +}; + +TEST_P(utils_encryption_read_encrypted_range_fixture, + within_chunk_data_buffer) { + std::uint64_t end_cap = chunk ? static_cast(chunk) - 1U : 0U; + std::uint64_t begin = 123U; + std::uint64_t end = 4567U; + if (end > end_cap) { + end = end_cap; + } + + if (end < begin) { + begin = end; + } + + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want( + plain.begin() + static_cast(begin), + plain.begin() + static_cast(end) + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + cross_chunk_boundary_data_buffer) { + std::uint64_t begin = static_cast(chunk) - 512U; + std::uint64_t end = begin + 1024U - 1U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want( + plain.begin() + static_cast(begin), + plain.begin() + static_cast(end) + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + multi_chunk_span_data_buffer) { + std::uint64_t begin = static_cast(chunk) - 10U; + std::uint64_t end = static_cast(2U * chunk) + 19U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want( + plain.begin() + static_cast(begin), + plain.begin() + static_cast(end) + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + tail_of_file_data_buffer) { + std::uint64_t begin = static_cast(plain_sz) - 200U; + std::uint64_t end = static_cast(plain_sz) - 1U; + ASSERT_GE(end, begin); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want( + plain.begin() + static_cast(begin), + plain.begin() + static_cast(end) + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, whole_file_data_buffer) { + std::uint64_t begin = 0U; + std::uint64_t end = static_cast(plain_sz - 1U); + ASSERT_GE(end, begin); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + EXPECT_EQ(out, plain); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + pointer_sink_cross_chunk_with_array) { + std::uint64_t begin = static_cast(chunk) - 256U; + constexpr std::size_t data_len = 2048U; + std::uint64_t end = begin + data_len - 1U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + + std::array sink{}; + std::size_t bytes_read = 0U; + + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, sink.data(), sink.size(), + bytes_read)); + EXPECT_EQ(bytes_read, sink.size()); + + std::vector want( + plain.begin() + static_cast(begin), + plain.begin() + static_cast(end) + 1U); + EXPECT_TRUE(std::equal(sink.begin(), sink.end(), want.begin(), want.end())); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + reader_failure_for_both_overloads) { + std::size_t call_count = 0U; + auto flaky_reader = [this, &call_count](data_buffer &out, std::uint64_t start, + std::uint64_t end) -> bool { + if (++call_count == 1U) { + return false; + } + auto len = static_cast(end - start + 1U); + out.assign( + std::next(cipher_blob.begin(), static_cast(start)), + std::next(cipher_blob.begin(), + static_cast(start + len))); + return true; + }; + + std::uint64_t begin = 0U; + constexpr std::size_t data_len = 1024U; + std::uint64_t end = begin + data_len - 1U; + http_range range{begin, end}; + + { + data_buffer out; + EXPECT_FALSE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, flaky_reader, total_size, out)); + EXPECT_TRUE(out.empty()); + } + + call_count = 0U; + { + std::array buf{}; + std::size_t bytes_read = 0U; + EXPECT_FALSE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, flaky_reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 0U); + } +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + invalid_range_end_before_begin) { + std::uint64_t begin = 100U; + std::uint64_t end = 99U; + http_range range{begin, end}; + + { + data_buffer out; + EXPECT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, out)); + EXPECT_TRUE(out.empty()); + } + + { + std::array buf{}; + std::size_t bytes_read = 0U; + EXPECT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 0U); + } +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, single_byte_read) { + std::uint64_t pos = 777U; + if (pos >= plain_sz) { + pos = plain_sz ? static_cast(plain_sz) - 1U : 0U; + } + + http_range range{pos, pos}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + ASSERT_EQ(out.size(), 1U); + EXPECT_EQ(out[0], plain[pos]); + + std::array buf{}; + std::size_t bytes_read = 0U; + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 1U); + EXPECT_EQ(buf[0], plain[pos]); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + begin_at_exact_chunk_boundary) { + + auto begin = static_cast(chunk); + std::uint64_t end = begin + 1024U - 1U; + if (end >= plain_sz) + end = static_cast(plain_sz) - 1U; + ASSERT_GE(end, begin); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + std::vector want( + plain.begin() + static_cast(begin), + plain.begin() + static_cast(end) + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, last_byte_only) { + std::uint64_t pos = plain_sz ? static_cast(plain_sz) - 1U : 0U; + http_range range{pos, pos}; + data_buffer out; + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + ASSERT_EQ(out.size(), 1U); + EXPECT_EQ(out[0], plain[pos]); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, tiny_file_whole_read) { + plain = make_random_plain(37U); + std::tie(cipher_blob, total_size) = + build_encrypted_blob(plain, key, uses_kdf, kdf); + reader = make_reader(cipher_blob); + + http_range range{0U, static_cast(plain.size() - 1U)}; + data_buffer out; + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + EXPECT_EQ(out, plain); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + pointer_sink_exact_small_window) { + std::uint64_t begin = 5U; + std::uint64_t end = begin + 7U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + http_range range{begin, end}; + + std::array sink{}; + std::size_t bytes_read = 0U; + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, sink.data(), sink.size(), + bytes_read)); + EXPECT_EQ(bytes_read, sink.size()); + EXPECT_TRUE(std::equal(sink.begin(), sink.end(), + plain.begin() + static_cast(begin))); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + range_past_eof_truncates) { + std::uint64_t begin = static_cast(plain_sz) - 10U; + std::uint64_t end = static_cast(plain_sz); + http_range range{begin, end}; + + data_buffer out; + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::size_t expected_len = + static_cast(static_cast(plain_sz) - begin); + std::vector want( + plain.begin() + static_cast(begin), + plain.begin() + static_cast(plain_sz)); + ASSERT_EQ(out.size(), expected_len); + EXPECT_EQ(out, want); + + std::array buf{}; + std::size_t bytes_read = 0U; + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, std::min(buf.size(), expected_len)); + EXPECT_TRUE(std::equal(buf.begin(), buf.begin() + bytes_read, + plain.begin() + static_cast(begin))); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + pointer_sink_larger_buffer) { + std::uint64_t begin = 42U; + std::uint64_t end = begin + 63U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + http_range range{begin, end}; + + std::array buf{}; + std::size_t bytes_read = 0U; + + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 64U); + EXPECT_TRUE(std::equal(buf.begin(), buf.begin() + 64U, + plain.begin() + static_cast(begin))); +} + +INSTANTIATE_TEST_SUITE_P(no_kdf_and_kdf, + utils_encryption_read_encrypted_range_fixture, + ::testing::Values(false, true)); +} // namespace monitarr + +#endif // defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST) diff --git a/support/test/src/utils/encryption_test.cpp b/support/test/src/utils/encryption_test.cpp index 663fa7b..9bb5d33 100644 --- a/support/test/src/utils/encryption_test.cpp +++ b/support/test/src/utils/encryption_test.cpp @@ -24,6 +24,10 @@ #if defined(PROJECT_ENABLE_LIBSODIUM) namespace { +#if defined(PROJECT_ENABLE_BOOST) +const std::string buffer = "cow moose dog chicken"; +#endif // defined(PROJECT_ENABLE_BOOST) + const auto get_stop_requested = []() -> bool { return false; }; } // namespace @@ -32,24 +36,21 @@ static constexpr std::string_view token{"moose"}; static constexpr std::wstring_view token_w{L"moose"}; TEST(utils_encryption, generate_key) { - auto key1 = - utils::encryption::generate_key(token); + auto key1 = utils::encryption::generate_key(token); EXPECT_STREQ( "ab4a0b004e824962913f7c0f79582b6ec7a3b8726426ca61d1a0a28ce5049e96", utils::collection::to_hex_string(key1).c_str()); - auto key2 = - utils::encryption::generate_key("moose"); - auto key3 = - utils::encryption::generate_key("moose"); + auto key2 = utils::encryption::generate_key("moose"); + auto key3 = utils::encryption::generate_key("moose"); EXPECT_EQ(key2, key3); auto key4 = - utils::encryption::generate_key("moose2"); + utils::encryption::generate_key("moose2"); EXPECT_NE(key2, key4); auto key1_w = - utils::encryption::generate_key(token_w); + utils::encryption::generate_key(token_w); EXPECT_NE(key1, key1_w); #if defined(_WIN32) EXPECT_STREQ( @@ -62,34 +63,33 @@ TEST(utils_encryption, generate_key) { #endif auto key2_w = - utils::encryption::generate_key(L"moose"); + utils::encryption::generate_key(L"moose"); auto key3_w = - utils::encryption::generate_key(L"moose"); + utils::encryption::generate_key(L"moose"); EXPECT_EQ(key2_w, key3_w); EXPECT_NE(key2_w, key2); EXPECT_NE(key3_w, key3); auto key4_w = - utils::encryption::generate_key(L"moose2"); + utils::encryption::generate_key(L"moose2"); EXPECT_NE(key2_w, key4_w); EXPECT_NE(key4_w, key4); } TEST(utils_encryption, generate_key_default_hasher_is_blake2b_256) { - auto key1 = - utils::encryption::generate_key(token); - auto key2 = utils::encryption::generate_key( + auto key1 = utils::encryption::generate_key(token); + auto key2 = utils::encryption::generate_key( token, [](auto &&data, auto &&size) -> auto { - return utils::encryption::create_hash_blake2b_256( + return utils::hash::create_hash_blake2b_256( std::string_view(reinterpret_cast(data), size)); }); EXPECT_EQ(key1, key2); auto key1_w = - utils::encryption::generate_key(token_w); - auto key2_w = utils::encryption::generate_key( + utils::encryption::generate_key(token_w); + auto key2_w = utils::encryption::generate_key( token_w, [](auto &&data, auto &&size) -> auto { - return utils::encryption::create_hash_blake2b_256(std::wstring_view( + return utils::hash::create_hash_blake2b_256(std::wstring_view( reinterpret_cast(data), size / sizeof(wchar_t))); }); EXPECT_EQ(key1_w, key2_w); @@ -99,22 +99,22 @@ TEST(utils_encryption, generate_key_default_hasher_is_blake2b_256) { } TEST(utils_encryption, generate_key_with_hasher) { - auto key1 = utils::encryption::generate_key( - token, utils::encryption::blake2b_256_hasher); + auto key1 = utils::encryption::generate_key( + token, utils::hash::blake2b_256_hasher); EXPECT_STREQ( "ab4a0b004e824962913f7c0f79582b6ec7a3b8726426ca61d1a0a28ce5049e96", utils::collection::to_hex_string(key1).c_str()); - auto key2 = utils::encryption::generate_key( - token, utils::encryption::sha256_hasher); + auto key2 = utils::encryption::generate_key( + token, utils::hash::sha256_hasher); EXPECT_NE(key1, key2); EXPECT_STREQ( "182072537ada59e4d6b18034a80302ebae935f66adbdf0f271d3d36309c2d481", utils::collection::to_hex_string(key2).c_str()); - auto key1_w = utils::encryption::generate_key( - token_w, utils::encryption::blake2b_256_hasher); + auto key1_w = utils::encryption::generate_key( + token_w, utils::hash::blake2b_256_hasher); #if defined(_WIN32) EXPECT_STREQ( L"4f5eb2a2ab34e3777b230465283923080b9ba59311e74058ccd74185131d11fe", @@ -125,8 +125,8 @@ TEST(utils_encryption, generate_key_with_hasher) { utils::collection::to_hex_wstring(key1_w).c_str()); #endif - auto key2_w = utils::encryption::generate_key( - token_w, utils::encryption::sha256_hasher); + auto key2_w = utils::encryption::generate_key( + token_w, utils::hash::sha256_hasher); EXPECT_NE(key1_w, key2_w); #if defined(_WIN32) @@ -144,7 +144,69 @@ TEST(utils_encryption, generate_key_with_hasher) { } #if defined(PROJECT_ENABLE_BOOST) -static const std::string buffer = "cow moose dog chicken"; +TEST(utils_encryption, generate_argon2id_key) { + utils::encryption::kdf_config cfg; + + { + auto key1 = + utils::encryption::generate_key(token, cfg); + + auto key2 = + utils::encryption::generate_key(token, cfg); + EXPECT_NE(key1, key2); + + auto key3 = + utils::encryption::generate_key(token, cfg); + EXPECT_NE(key3, key1); + + auto key4 = + utils::encryption::generate_key(token, cfg); + EXPECT_NE(key4, key2); + + EXPECT_NE(key3, key4); + } + + { + auto key1 = + utils::encryption::generate_key(token_w, cfg); + + auto key2 = + utils::encryption::generate_key(token_w, cfg); + EXPECT_NE(key1, key2); + + auto key3 = + utils::encryption::generate_key(token_w, cfg); + EXPECT_NE(key3, key1); + + auto key4 = + utils::encryption::generate_key(token_w, cfg); + EXPECT_NE(key4, key2); + + EXPECT_NE(key3, key4); + } +} + +TEST(utils_encryption, recreate_argon2id_key) { + utils::encryption::kdf_config cfg; + + { + auto key1 = + utils::encryption::generate_key(token, cfg); + + auto key2 = + utils::encryption::recreate_key(token, cfg); + EXPECT_EQ(key1, key2); + } + + { + auto key1 = + utils::encryption::generate_key(token_w, cfg); + + auto key2 = + utils::encryption::recreate_key(token_w, cfg); + EXPECT_EQ(key1, key2); + } +} static void test_encrypted_result(const data_buffer &result) { EXPECT_EQ(buffer.size() + utils::encryption::encryption_header_size, @@ -155,6 +217,17 @@ static void test_encrypted_result(const data_buffer &result) { EXPECT_STREQ(buffer.c_str(), data.c_str()); } +static void +test_encrypted_result_using_argon2id(const data_buffer &result, + const utils::encryption::kdf_config &cfg) { + EXPECT_EQ(buffer.size() + utils::encryption::encryption_header_size, + result.size()); + std::string data; + EXPECT_TRUE(utils::encryption::decrypt_data(token, cfg, result, data)); + EXPECT_EQ(buffer.size(), data.size()); + EXPECT_STREQ(buffer.c_str(), data.c_str()); +} + TEST(utils_encryption, encrypt_data_buffer) { data_buffer result; utils::encryption::encrypt_data(token, buffer, result); @@ -163,7 +236,7 @@ TEST(utils_encryption, encrypt_data_buffer) { TEST(utils_encryption, encrypt_data_buffer_with_key) { const auto key = - utils::encryption::generate_key(token); + utils::encryption::generate_key(token); data_buffer result; utils::encryption::encrypt_data(key, buffer, result); test_encrypted_result(result); @@ -179,7 +252,7 @@ TEST(utils_encryption, encrypt_data_pointer) { TEST(utils_encryption, encrypt_data_pointer_with_key) { const auto key = - utils::encryption::generate_key(token); + utils::encryption::generate_key(token); data_buffer result; utils::encryption::encrypt_data( key, reinterpret_cast(buffer.data()), @@ -189,7 +262,7 @@ TEST(utils_encryption, encrypt_data_pointer_with_key) { TEST(utils_encryption, decrypt_data_pointer) { const auto key = - utils::encryption::generate_key(token); + utils::encryption::generate_key(token); data_buffer result; utils::encryption::encrypt_data( key, reinterpret_cast(buffer.data()), @@ -205,7 +278,7 @@ TEST(utils_encryption, decrypt_data_pointer) { TEST(utils_encryption, decrypt_data_buffer_with_key) { const auto key = - utils::encryption::generate_key(token); + utils::encryption::generate_key(token); data_buffer result; utils::encryption::encrypt_data( key, reinterpret_cast(buffer.data()), @@ -220,7 +293,7 @@ TEST(utils_encryption, decrypt_data_buffer_with_key) { TEST(utils_encryption, decrypt_data_pointer_with_key) { const auto key = - utils::encryption::generate_key(token); + utils::encryption::generate_key(token); data_buffer result; utils::encryption::encrypt_data( key, reinterpret_cast(buffer.data()), @@ -236,7 +309,7 @@ TEST(utils_encryption, decrypt_data_pointer_with_key) { TEST(utils_encryption, decryption_failure) { const auto key = - utils::encryption::generate_key(token); + utils::encryption::generate_key(token); data_buffer result; utils::encryption::encrypt_data( key, reinterpret_cast(buffer.data()), @@ -280,6 +353,32 @@ TEST(utils_encryption, decrypt_file_path) { EXPECT_STREQ("/moose/cow/test.dat", file_path.c_str()); } } + +TEST(utils_encryption, encrypt_data_buffer_using_argon2id) { + utils::encryption::kdf_config cfg; + + data_buffer result; + utils::encryption::encrypt_data(token, cfg, buffer, result); + test_encrypted_result_using_argon2id(result, cfg); +} + +TEST(utils_encryption, encrypt_data_pointer_using_argon2id) { + utils::encryption::kdf_config cfg; + + data_buffer result; + utils::encryption::encrypt_data( + token, cfg, reinterpret_cast(buffer.data()), + buffer.size(), result); + test_encrypted_result_using_argon2id(result, cfg); +} + +// TEST(utils_encryption, decrypt_file_name_using_argon2id) {} + +// TEST(utils_encryption, decrypt_file_path_using_argon2id) {} +// +// TEST(utils_encryption, decrypt_file_name_using_argon2id_master_key) {} + +// TEST(utils_encryption, decrypt_file_path_using_argon2id_master_key) {} #endif // defined(PROJECT_ENABLE_BOOST) } // namespace monitarr diff --git a/support/test/src/utils/file_test.cpp b/support/test/src/utils/file_test.cpp index 9027efc..aae099d 100644 --- a/support/test/src/utils/file_test.cpp +++ b/support/test/src/utils/file_test.cpp @@ -510,6 +510,7 @@ TEST(utils_file, get_times) { utils::file::get_times(test::create_random_file(1U).get_path()); EXPECT_TRUE(times.has_value()); EXPECT_LT(0U, times->get(utils::file::time_type::accessed)); + EXPECT_LT(0U, times->get(utils::file::time_type::changed)); EXPECT_LT(0U, times->get(utils::file::time_type::created)); EXPECT_LT(0U, times->get(utils::file::time_type::modified)); EXPECT_LT(0U, times->get(utils::file::time_type::written)); @@ -520,6 +521,7 @@ TEST(utils_file, get_times) { utils::string::from_utf8(test::create_random_file(1U).get_path())); EXPECT_TRUE(times.has_value()); EXPECT_LT(0U, times->get(utils::file::time_type::accessed)); + EXPECT_LT(0U, times->get(utils::file::time_type::changed)); EXPECT_LT(0U, times->get(utils::file::time_type::created)); EXPECT_LT(0U, times->get(utils::file::time_type::modified)); EXPECT_LT(0U, times->get(utils::file::time_type::written)); @@ -540,6 +542,11 @@ TEST(utils_file, get_time) { EXPECT_TRUE(file_time.has_value()); EXPECT_LT(0U, file_time.value()); + file_time = + utils::file::get_time(file_path, utils::file::time_type::changed); + EXPECT_TRUE(file_time.has_value()); + EXPECT_LT(0U, file_time.value()); + file_time = utils::file::get_time(file_path, utils::file::time_type::created); EXPECT_TRUE(file_time.has_value()); @@ -565,6 +572,11 @@ TEST(utils_file, get_time) { EXPECT_TRUE(file_time.has_value()); EXPECT_LT(0U, file_time.value()); + file_time = + utils::file::get_time(file_path, utils::file::time_type::changed); + EXPECT_TRUE(file_time.has_value()); + EXPECT_LT(0U, file_time.value()); + file_time = utils::file::get_time(file_path, utils::file::time_type::created); EXPECT_TRUE(file_time.has_value()); diff --git a/support/test/src/utils/hash_test.cpp b/support/test/src/utils/hash_test.cpp index a46d4e4..71029a7 100644 --- a/support/test/src/utils/hash_test.cpp +++ b/support/test/src/utils/hash_test.cpp @@ -25,34 +25,97 @@ namespace monitarr { TEST(utils_hash, hash_type_sizes) { - EXPECT_EQ(32U, utils::encryption::hash_256_t{}.size()); - EXPECT_EQ(48U, utils::encryption::hash_384_t{}.size()); - EXPECT_EQ(64U, utils::encryption::hash_512_t{}.size()); + EXPECT_EQ(4U, utils::hash::hash_32_t{}.size()); + EXPECT_EQ(8U, utils::hash::hash_64_t{}.size()); + EXPECT_EQ(16U, utils::hash::hash_128_t{}.size()); + EXPECT_EQ(32U, utils::hash::hash_256_t{}.size()); + EXPECT_EQ(48U, utils::hash::hash_384_t{}.size()); + EXPECT_EQ(64U, utils::hash::hash_512_t{}.size()); } TEST(utils_hash, default_hasher_is_blake2b) { - EXPECT_EQ( - &utils::encryption::blake2b_256_hasher, - &utils::encryption::default_create_hash()); + EXPECT_EQ(&utils::hash::blake2b_32_hasher, + &utils::hash::default_create_hash()); - EXPECT_EQ( - &utils::encryption::blake2b_384_hasher, - &utils::encryption::default_create_hash()); + EXPECT_EQ(&utils::hash::blake2b_64_hasher, + &utils::hash::default_create_hash()); - EXPECT_EQ( - &utils::encryption::blake2b_512_hasher, - &utils::encryption::default_create_hash()); + EXPECT_EQ(&utils::hash::blake2b_128_hasher, + &utils::hash::default_create_hash()); + + EXPECT_EQ(&utils::hash::blake2b_256_hasher, + &utils::hash::default_create_hash()); + + EXPECT_EQ(&utils::hash::blake2b_384_hasher, + &utils::hash::default_create_hash()); + + EXPECT_EQ(&utils::hash::blake2b_512_hasher, + &utils::hash::default_create_hash()); +} + +TEST(utils_hash, blake2b_32) { + auto hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_32("a")); + EXPECT_STREQ("ca234c55", hash.c_str()); + + hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_32(L"a")); +#if defined(_WIN32) + EXPECT_STREQ("4c368117", hash.c_str()); +#else // !defined(_WIN32) + EXPECT_STREQ("02a631b8", hash.c_str()); +#endif + + hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_32({1U})); + EXPECT_STREQ("593bda73", hash.c_str()); +} + +TEST(utils_hash, blake2b_64) { + auto hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_64("a")); + EXPECT_STREQ("40f89e395b66422f", hash.c_str()); + + hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_64(L"a")); +#if defined(_WIN32) + EXPECT_STREQ("4dd0bb1c45b748c1", hash.c_str()); +#else // !defined(_WIN32) + EXPECT_STREQ("85ff8cc55b79d38a", hash.c_str()); +#endif + + hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_64({1U})); + EXPECT_STREQ("00e83d0a3f7519ad", hash.c_str()); +} + +TEST(utils_hash, blake2b_128) { + auto hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_128("a")); + EXPECT_STREQ("27c35e6e9373877f29e562464e46497e", hash.c_str()); + + hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_128(L"a")); +#if defined(_WIN32) + EXPECT_STREQ("396660e76c84bb7786f979f10b58fa79", hash.c_str()); +#else // !defined(_WIN32) + EXPECT_STREQ("dae64afb310a3426ad84f0739fde5cef", hash.c_str()); +#endif + + hash = utils::collection::to_hex_string( + utils::hash::create_hash_blake2b_128({1U})); + EXPECT_STREQ("4a9e6f9b8d43f6ad008f8c291929dee2", hash.c_str()); } TEST(utils_hash, blake2b_256) { auto hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_256("a")); + utils::hash::create_hash_blake2b_256("a")); EXPECT_STREQ( "8928aae63c84d87ea098564d1e03ad813f107add474e56aedd286349c0c03ea4", hash.c_str()); hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_256(L"a")); + utils::hash::create_hash_blake2b_256(L"a")); #if defined(_WIN32) EXPECT_STREQ( "d2373b17cd8a8e19e39f52fa4905a274f93805fbb8bb4c7f3cb4b2cd6708ec8a", @@ -64,7 +127,7 @@ TEST(utils_hash, blake2b_256) { #endif hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_256({1U})); + utils::hash::create_hash_blake2b_256({1U})); EXPECT_STREQ( "ee155ace9c40292074cb6aff8c9ccdd273c81648ff1149ef36bcea6ebb8a3e25", hash.c_str()); @@ -72,13 +135,13 @@ TEST(utils_hash, blake2b_256) { TEST(utils_hash, blake2b_384) { auto hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_384("a")); + utils::hash::create_hash_blake2b_384("a")); EXPECT_STREQ("7d40de16ff771d4595bf70cbda0c4ea0a066a6046fa73d34471cd4d93d827d7" "c94c29399c50de86983af1ec61d5dcef0", hash.c_str()); hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_384(L"a")); + utils::hash::create_hash_blake2b_384(L"a")); #if defined(_WIN32) EXPECT_STREQ("637fe31d1e955760ef31043d525d9321826a778ddbe82fcde45a98394241380" "96675e2f87e36b53ab223a7fd254198fd", @@ -90,7 +153,7 @@ TEST(utils_hash, blake2b_384) { #endif hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_384({1U})); + utils::hash::create_hash_blake2b_384({1U})); EXPECT_STREQ("42cfe875d08d816538103b906bb0b05202e0b09c4e981680c1110684fc7845b" "c91c178fa167afcc445490644b2bf5f5b", hash.c_str()); @@ -98,14 +161,14 @@ TEST(utils_hash, blake2b_384) { TEST(utils_hash, blake2b_512) { auto hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_512("a")); + utils::hash::create_hash_blake2b_512("a")); EXPECT_STREQ( "333fcb4ee1aa7c115355ec66ceac917c8bfd815bf7587d325aec1864edd24e34d5abe2c6" "b1b5ee3face62fed78dbef802f2a85cb91d455a8f5249d330853cb3c", hash.c_str()); hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_512(L"a")); + utils::hash::create_hash_blake2b_512(L"a")); #if defined(_WIN32) EXPECT_STREQ( "05970b95468b0b1941066ff189091493e73859ce41cde5ad08118e93ea1d81a57a144296" @@ -119,7 +182,7 @@ TEST(utils_hash, blake2b_512) { #endif hash = utils::collection::to_hex_string( - utils::encryption::create_hash_blake2b_512({1U})); + utils::hash::create_hash_blake2b_512({1U})); EXPECT_STREQ( "9545ba37b230d8a2e716c4707586542780815b7c4088edcb9af6a9452d50f32474d5ba9a" "ab52a67aca864ef2696981c2eadf49020416136afd838fb048d21653", @@ -127,14 +190,14 @@ TEST(utils_hash, blake2b_512) { } TEST(utils_hash, sha256) { - auto hash = utils::collection::to_hex_string( - utils::encryption::create_hash_sha256("a")); + auto hash = + utils::collection::to_hex_string(utils::hash::create_hash_sha256("a")); EXPECT_STREQ( "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", hash.c_str()); - hash = utils::collection::to_hex_string( - utils::encryption::create_hash_sha256(L"a")); + hash = + utils::collection::to_hex_string(utils::hash::create_hash_sha256(L"a")); #if defined(_WIN32) EXPECT_STREQ( "ffe9aaeaa2a2d5048174df0b80599ef0197ec024c4b051bc9860cff58ef7f9f3", @@ -145,23 +208,23 @@ TEST(utils_hash, sha256) { hash.c_str()); #endif - hash = utils::collection::to_hex_string( - utils::encryption::create_hash_sha256({1U})); + hash = + utils::collection::to_hex_string(utils::hash::create_hash_sha256({1U})); EXPECT_STREQ( "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a", hash.c_str()); } TEST(utils_hash, sha512) { - auto hash = utils::collection::to_hex_string( - utils::encryption::create_hash_sha512("a")); + auto hash = + utils::collection::to_hex_string(utils::hash::create_hash_sha512("a")); EXPECT_STREQ( "1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c65" "2bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75", hash.c_str()); - hash = utils::collection::to_hex_string( - utils::encryption::create_hash_sha512(L"a")); + hash = + utils::collection::to_hex_string(utils::hash::create_hash_sha512(L"a")); #if defined(_WIN32) EXPECT_STREQ( "5c2ca3d50f46ece6066c53bd1a490cbe5f72d2738ae9417332e91e5c3f75205c639d71a9" @@ -174,8 +237,8 @@ TEST(utils_hash, sha512) { hash.c_str()); #endif - hash = utils::collection::to_hex_string( - utils::encryption::create_hash_sha512({1U})); + hash = + utils::collection::to_hex_string(utils::hash::create_hash_sha512({1U})); EXPECT_STREQ( "7b54b66836c1fbdd13d2441d9e1434dc62ca677fb68f5fe66a464baadecdbd00576f8d6b" "5ac3bcc80844b7d50b1cc6603444bbe7cfcf8fc0aa1ee3c636d9e339", diff --git a/support/test/src/utils/string_test.cpp b/support/test/src/utils/string_test.cpp index bfb4fb6..bccd7bd 100644 --- a/support/test/src/utils/string_test.cpp +++ b/support/test/src/utils/string_test.cpp @@ -134,4 +134,10 @@ TEST(utils_string, to_bool) { EXPECT_FALSE(utils::string::to_bool("0")); EXPECT_FALSE(utils::string::to_bool("00000.00000")); } + +TEST(utils_string, utf8_string_conversion) { + std::wstring ws = L"Hello 🌍 — 𝄞 漢字"; + std::wstring ws2 = utils::string::from_utf8(utils::string::to_utf8(ws)); + EXPECT_STREQ(ws.c_str(), ws2.c_str()); +} } // namespace monitarr diff --git a/support/test/src/utils/ttl_cache_test.cpp b/support/test/src/utils/ttl_cache_test.cpp new file mode 100644 index 0000000..8eafcbd --- /dev/null +++ b/support/test/src/utils/ttl_cache_test.cpp @@ -0,0 +1,253 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include "test.hpp" + +namespace monitarr { +TEST(utils_ttl_cache, can_construct_cache) { + utils::ttl_cache cache; + EXPECT_EQ(utils::ttl_cache::default_expiration, + cache.get_ttl()); +} + +TEST(utils_ttl_cache, can_construct_cache_with_ttl) { + utils::ttl_cache cache(std::chrono::milliseconds(1000U)); + EXPECT_EQ(std::chrono::milliseconds(1000U), cache.get_ttl()); +} + +TEST(utils_ttl_cache, can_change_ttl) { + utils::ttl_cache cache; + cache.set_ttl(std::chrono::milliseconds(1000U)); + EXPECT_EQ(std::chrono::milliseconds(1000U), cache.get_ttl()); +} + +TEST(utils_ttl_cache, can_set_and_get) { + utils::ttl_cache cache; + cache.set("/test", 21U); + auto data = cache.get("/test"); + ASSERT_NE(nullptr, data.get()); + + EXPECT_EQ(21U, data->load()); +} + +TEST(utils_ttl_cache, get_returns_nullptr_for_api_path_not_in_cache) { + utils::ttl_cache cache; + auto data = cache.get("/test"); + ASSERT_EQ(nullptr, data.get()); +} + +TEST(utils_ttl_cache, set_and_get_returns_value_and_refreshes_ttl) { + utils::ttl_cache cache(std::chrono::milliseconds(1000U)); + + cache.set("/test", 7U); + auto data = cache.get("/test"); + { + EXPECT_TRUE(cache.contains("/test")); + ASSERT_NE(data, nullptr); + EXPECT_EQ(7U, data->load()); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(200U)); + { + EXPECT_TRUE(cache.contains("/test")); + auto data2 = cache.get("/test"); + ASSERT_NE(data2, nullptr); + ASSERT_EQ(data.get(), data2.get()); + EXPECT_EQ(7U, data2->load()); + } + + { + std::this_thread::sleep_for(std::chrono::milliseconds(800U)); + cache.purge_expired(); + + auto data3 = cache.get("/test"); + EXPECT_TRUE(cache.contains("/test")); + + ASSERT_NE(data3, nullptr); + ASSERT_EQ(data.get(), data3.get()); + EXPECT_EQ(7U, data3->load()); + } +} + +TEST(utils_ttl_cache, entry_expires_without_refresh) { + utils::ttl_cache cache(std::chrono::milliseconds(50U)); + cache.set("/test", 42U); + + std::this_thread::sleep_for(std::chrono::milliseconds(51U)); + cache.purge_expired(); + EXPECT_FALSE(cache.contains("/test")); + + auto data = cache.get("/test"); + EXPECT_EQ(nullptr, data.get()); +} + +TEST(utils_ttl_cache, can_erase) { + utils::ttl_cache cache(std::chrono::milliseconds(50U)); + cache.set("/test", 42U); + cache.erase("/test"); + + EXPECT_FALSE(cache.contains("/test")); + auto data = cache.get("/test"); + EXPECT_EQ(nullptr, data.get()); +} + +TEST(utils_ttl_cache, can_clear) { + utils::ttl_cache cache(std::chrono::milliseconds(50U)); + + cache.set("/test", 42U); + cache.set("/test2", 42U); + EXPECT_TRUE(cache.contains("/test")); + EXPECT_TRUE(cache.contains("/test2")); + cache.clear(); + + { + EXPECT_FALSE(cache.contains("/test")); + auto data = cache.get("/test"); + EXPECT_EQ(nullptr, data.get()); + } + + { + EXPECT_FALSE(cache.contains("/test2")); + auto data = cache.get("/test2"); + EXPECT_EQ(nullptr, data.get()); + } +} + +TEST(utils_ttl_cache, can_handle_concurrent_access) { + utils::ttl_cache cache(std::chrono::milliseconds(5000U)); + + std::atomic start{false}; + std::thread writer([&] { + while (not start.load()) { + } + for (std::uint8_t ttl = 0U; ttl < 100U; ++ttl) { + cache.set("/key", ttl); + std::this_thread::yield(); + } + }); + + std::thread reader([&] { + while (not start.load()) { + } + for (std::uint8_t ttl = 0U; ttl < 100U; ++ttl) { + auto data = cache.get("/key"); + if (data) { + [[maybe_unused]] auto res = data->load(); + } + std::this_thread::yield(); + } + }); + + start = true; + writer.join(); + reader.join(); + + auto data = cache.get("/key"); + ASSERT_NE(data, nullptr); + [[maybe_unused]] auto res = data->load(); +} + +TEST(utils_ttl_cache, can_handle_custom_atomic) { + utils::ttl_cache cache( + std::chrono::milliseconds(5000U)); + cache.set("/test", "test"); + auto data = cache.get("/test"); + ASSERT_NE(nullptr, data.get()); + EXPECT_STREQ("test", data->load().c_str()); +} + +TEST(utils_ttl_cache, get_renews_after_ttl_if_purge_expired_is_not_called) { + utils::ttl_cache cache(std::chrono::milliseconds(50U)); + cache.set("/test", 9U); + + std::this_thread::sleep_for(std::chrono::milliseconds(75U)); + + auto data = cache.get("/test"); + ASSERT_NE(nullptr, data.get()); + EXPECT_EQ(9U, data->load()); + + cache.purge_expired(); + EXPECT_TRUE(cache.contains("/test")); +} + +TEST(utils_ttl_cache, can_update_data) { + utils::ttl_cache cache; + + cache.set("/test", 1U); + auto data = cache.get("/test"); + ASSERT_NE(nullptr, data.get()); + EXPECT_EQ(1U, data->load()); + + cache.set("/test", 2U); + auto data2 = cache.get("/test"); + ASSERT_NE(nullptr, data2.get()); + EXPECT_EQ(data.get(), data2.get()); + EXPECT_EQ(2U, data2->load()); +} + +TEST(utils_ttl_cache, purge_expired_removes_only_expired_entries) { + utils::ttl_cache cache(std::chrono::milliseconds(1000U)); + cache.set("/test1", 1U); + cache.set("/test2", 2U); + + std::this_thread::sleep_for(std::chrono::milliseconds(500U)); + auto data = cache.get("/test2"); + ASSERT_NE(data, nullptr); + + std::this_thread::sleep_for(std::chrono::milliseconds(501U)); + cache.purge_expired(); + + EXPECT_FALSE(cache.contains("/test1")); + EXPECT_TRUE(cache.contains("/test2")); +} + +TEST(utils_ttl_cache, can_handle_non_existing_items_without_failure) { + utils::ttl_cache cache; + cache.set("/exists", 5U); + EXPECT_TRUE(cache.contains("/exists")); + + cache.erase("/not_found"); + EXPECT_TRUE(cache.contains("/exists")); + + auto data = cache.get("/exists"); + ASSERT_NE(nullptr, data.get()); + EXPECT_EQ(5U, data->load()); +} + +TEST(utils_ttl_cache, changing_ttl_affects_only_future_expirations) { + utils::ttl_cache cache(std::chrono::milliseconds(1000U)); + cache.set("/test", 11U); + + cache.set_ttl(std::chrono::milliseconds(100U)); + + std::this_thread::sleep_for(std::chrono::milliseconds(200U)); + cache.purge_expired(); + EXPECT_TRUE(cache.contains("/test")); + + auto data = cache.get("/test"); + ASSERT_NE(nullptr, data.get()); + EXPECT_EQ(11U, data->load()); + + std::this_thread::sleep_for(std::chrono::milliseconds(200U)); + cache.purge_expired(); + EXPECT_FALSE(cache.contains("/test")); +} +} // namespace monitarr