diff options
-rw-r--r-- | CMakeLists.txt | 8 | ||||
-rw-r--r-- | README.md | 11 | ||||
-rw-r--r-- | include/jau/file_util.hpp | 41 | ||||
-rw-r--r-- | scripts/build.sh | 1 | ||||
-rwxr-xr-x | scripts/mksquashfs_test_data.sh | 7 | ||||
-rw-r--r-- | src/file_util.cpp | 140 | ||||
-rw-r--r-- | test/CMakeLists.txt | 32 | ||||
-rw-r--r-- | test/testsudo_fileutils02.cpp | 61 | ||||
-rw-r--r-- | test_data.sqfs | bin | 0 -> 4096 bytes |
9 files changed, 293 insertions, 8 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 853967b..488bfd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,14 @@ include_directories (${SYSTEM_USR_DIR}) add_subdirectory (src) +message(STATUS "${PROJECT_NAME} TEST_WITH_SUDO = ${TEST_WITH_SUDO} (pre-set)") +if(NOT DEFINED TEST_WITH_SUDO) + set(TEST_WITH_SUDO OFF) + message(STATUS "${PROJECT_NAME} TEST_WITH_SUDO ${TEST_WITH_SUDO} (default)") +else() + message(STATUS "${PROJECT_NAME} TEST_WITH_SUDO ${TEST_WITH_SUDO} (user)") +endif() + IF(BUILDJAVA) configure_file (${CMAKE_CURRENT_SOURCE_DIR}/java_base/manifest.txt.in ${CMAKE_CURRENT_BINARY_DIR}/java_base/manifest.txt) configure_file (${CMAKE_CURRENT_SOURCE_DIR}/java_jni/manifest.txt.in ${CMAKE_CURRENT_BINARY_DIR}/java_jni/manifest.txt) @@ -114,6 +114,17 @@ Building debug build: -DDEBUG=ON ~~~~~~~~~~~~~ +Add unit tests to build (default: disabled) +~~~~~~~~~~~~~ +-DBUILD_TESTING=ON +~~~~~~~~~~~~~ + +Add unit tests requiring `sudo` to build (default: disabled), +this option required `-DBUILD_TESTING=ON` to be effective. +~~~~~~~~~~~~~ +-DTEST_WITH_SUDO=ON +~~~~~~~~~~~~~ + Disable stripping native lib even in non debug build: ~~~~~~~~~~~~~ -DUSE_STRIP=OFF diff --git a/include/jau/file_util.hpp b/include/jau/file_util.hpp index efd04f0..0b119d8 100644 --- a/include/jau/file_util.hpp +++ b/include/jau/file_util.hpp @@ -851,6 +851,47 @@ namespace jau { */ bool copy(const std::string& source_path, const std::string& dest_path, const copy_options copts = copy_options::none) noexcept; + struct mount_ctx { + bool mounted; + std::string mount_point; + int loop_device_id; + + mount_ctx(const std::string& mount_point_, const int loop_device_id_) + : mounted(true), mount_point(mount_point_), loop_device_id(loop_device_id_) {} + + mount_ctx() + : mounted(false), mount_point(), loop_device_id(-1) {} + }; + + /** + * Attach the filesystem image named in `image_path` to `target_path`. + * + * This method requires root permissions. + * + * @param image_path path of image source file + * @param mount_point directory where `image_path` shall be attached to + * @param fs_type type of filesystem, e.g. `squashfs`, `tmpfs`, `iso9660`, etc. + * @param mountflags mount flags, e.g. `MS_LAZYTIME | MS_NOATIME | MS_RDONLY` for a read-only lazy-time and no-atime filesystem. + * @param fs_options special filesystem options + * @return mount_ctx structure containing mounted status etc + * + * @see umount() + */ + mount_ctx mount_image(const std::string& image_path, const std::string& mount_point, const std::string& fs_type, + const unsigned long mountflags, const std::string fs_options=""); + + /** + * Detach the given mount_ctc `context` + * + * This method requires root permissions. + * + * @param context mount_ctx previously attached via mount_image() + * @return true if successful, otherwise false + * + * @see mount_image() + */ + bool umount(const mount_ctx& context); + /**@}*/ } /* namespace fs */ diff --git a/scripts/build.sh b/scripts/build.sh index 696a0e7..f39f550 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -41,6 +41,7 @@ buildit() { # CLANG_ARGS="-DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++" cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILD_TESTING=ON -DUSE_LIBUNWIND=ON -DUSE_LIBCURL=ON .. + # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILD_TESTING=ON -DUSE_LIBUNWIND=ON -DUSE_LIBCURL=ON -DTEST_WITH_SUDO=ON .. # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILD_TESTING=ON -DDEBUG=ON -DUSE_LIBUNWIND=ON -DUSE_LIBCURL=ON .. # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILD_TESTING=ON .. diff --git a/scripts/mksquashfs_test_data.sh b/scripts/mksquashfs_test_data.sh new file mode 100755 index 0000000..59980b1 --- /dev/null +++ b/scripts/mksquashfs_test_data.sh @@ -0,0 +1,7 @@ +#! /bin/sh + +sdir=`dirname $(readlink -f $0)` +rootdir=`dirname $sdir` + +cd $rootdir +/usr/bin/mksquashfs test_data test_data.sqfs -comp lzo diff --git a/src/file_util.cpp b/src/file_util.cpp index 2db5318..c95a217 100644 --- a/src/file_util.cpp +++ b/src/file_util.cpp @@ -34,8 +34,10 @@ extern "C" { #include <sys/stat.h> #ifdef __linux__ #include <sys/sendfile.h> + #include <linux/loop.h> #endif #include <sys/types.h> + #include <sys/mount.h> #include <dirent.h> #include <fcntl.h> #include <unistd.h> @@ -1392,3 +1394,141 @@ bool jau::fs::copy(const std::string& source_path, const std::string& target_pat return jau::fs::visit(source_stats, topts, pv); } +jau::fs::mount_ctx jau::fs::mount_image(const std::string& image_path, const std::string& mount_point, const std::string& fs_type, + const unsigned long mountflags, const std::string fs_options) +{ + file_stats image_stats(image_path); + if( !image_stats.is_file()) { + return mount_ctx(); + } + file_stats target_stats(mount_point); + if( !target_stats.is_dir()) { + return mount_ctx(); + } + int backingfile = ::open64(image_stats.path().c_str(), O_RDWR); + if( 0 > backingfile ) { + jau::fprintf_td(stderr, "mount: Error: Couldn't open image-file '%s': res %d, errno %d (%s)\n", + image_stats.to_string().c_str(), backingfile, errno, ::strerror(errno)); + return mount_ctx(); + } +#ifdef __linux__ + int loop_device_id = -1; + int loop_ctl_fd = -1, loop_device_fd = -1; + char loopname[4096]; + void* fs_options_cstr = nullptr; + int mount_res = -1; + + loop_ctl_fd = ::open64("/dev/loop-control", O_RDWR); + if( 0 > loop_ctl_fd ) { + jau::fprintf_td(stderr, "mount: Error: Couldn't open loop-control: res %d, errno %d (%s)\n", + loop_ctl_fd, errno, ::strerror(errno)); + goto errout; + } + + loop_device_id = (int) ::ioctl(loop_ctl_fd, LOOP_CTL_GET_FREE); + if( 0 > loop_device_id ) { + jau::fprintf_td(stderr, "mount: Error: Couldn't get free loop-device: res %d, errno %d (%s)\n", + loop_device_id, errno, ::strerror(errno)); + goto errout; + } + ::close(loop_ctl_fd); + loop_ctl_fd = -1; + + snprintf(loopname, sizeof(loopname), "/dev/loop%d", loop_device_id); + jau::fprintf_td(stderr, "mount: Info: Using loop-device '%s'\n", loopname); + + loop_device_fd = ::open64(loopname, O_RDWR); + if( 0 > loop_device_fd ) { + jau::fprintf_td(stderr, "mount: Error: Couldn't open loop-device '%s': res %d, errno %d (%s)\n", + loopname, loop_device_fd, errno, ::strerror(errno)); + goto errout; + } + if( 0 > ::ioctl(loop_device_fd, LOOP_SET_FD, backingfile) ) { + jau::fprintf_td(stderr, "mount: Error: Couldn't attach image-file '%s' to loop-device '%s': errno %d (%s)\n", + image_stats.to_string().c_str(), loopname, errno, ::strerror(errno)); + goto errout; + } + ::close(loop_device_fd); + loop_device_fd = -1; + ::close(backingfile); + backingfile = -1; + + if( fs_options.size() > 0 ) { + fs_options_cstr = (void*) fs_options.data(); + } + mount_res = ::mount(loopname, target_stats.path().c_str(), fs_type.c_str(), mountflags, fs_options_cstr); + if( 0 != mount_res ) { + jau::fprintf_td(stderr, "mount: Error: source_path %s, target_path %s, fs_type %s, res %d, errno %d (%s)\n", + image_path.c_str(), mount_point.c_str(), fs_type.c_str(), mount_res, errno, ::strerror(errno)); + ::ioctl(loop_device_fd, LOOP_CLR_FD, 0); + goto errout; + } + return mount_ctx(mount_point, loop_device_id); + +errout: + if( 0 <= loop_ctl_fd ) { + ::close(loop_ctl_fd); + } + if( 0 <= loop_device_fd ) { + ::close(loop_device_fd); + } +#else + (void)fs_type; + (void)mountflags; + (void)fs_options; +#endif + if( 0 <= backingfile ) { + ::close(backingfile); + } + return mount_ctx(); +} + +bool jau::fs::umount(const mount_ctx& context) +{ + if( !context.mounted) { + return false; + } + file_stats target_stats(context.mount_point); + if( !target_stats.is_dir()) { + return false; + } + const int umount_res = ::umount(target_stats.path().c_str()); + if( 0 != umount_res ) { + jau::fprintf_td(stderr, "umount: Error: Couldn't umount '%s': res %d, errno %d (%s)\n", + target_stats.to_string().c_str(), umount_res, errno, ::strerror(errno)); + } + if( 0 > context.loop_device_id ) { + // mounted w/o loop-device, done + return 0 == umount_res; + } +#ifdef __linux__ + int loop_device_fd = -1; + char loopname[4096]; + + snprintf(loopname, sizeof(loopname), "/dev/loop%d", context.loop_device_id); + jau::fprintf_td(stderr, "umount: Info: Using loop-device '%s'\n", loopname); + + loop_device_fd = ::open64(loopname, O_RDWR); + if( 0 > loop_device_fd ) { + jau::fprintf_td(stderr, "umount: Error: Couldn't open loop-device '%s': res %d, errno %d (%s)\n", + loopname, loop_device_fd, errno, ::strerror(errno)); + goto errout; + } + if( 0 > ::ioctl(loop_device_fd, LOOP_CLR_FD, 0) ) { + jau::fprintf_td(stderr, "umount: Error: Couldn't deattach loop-device '%s': errno %d (%s)\n", + loopname, errno, ::strerror(errno)); + goto errout; + } + ::close(loop_device_fd); + loop_device_fd = -1; + + return 0 == umount_res; + +errout: + if( 0 <= loop_device_fd ) { + ::close(loop_device_fd); + } +#endif + return false; +} + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7ffa6b0..fdd2873 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,19 +13,35 @@ target_compile_options(catch2 PUBLIC "-DCATCH_AMALGAMATED_CUSTOM_MAIN=1") # install(TARGETS catch2 LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) # These examples use the standard separate compilation -file(GLOB_RECURSE SOURCES_IDIOMATIC_EXAMPLES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.cpp") +file(GLOB_RECURSE SOURCES_IDIOMATIC_TEST RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.cpp") -string( REPLACE ".cpp" "" BASENAMES_IDIOMATIC_EXAMPLES "${SOURCES_IDIOMATIC_EXAMPLES}" ) -set( TARGETS_IDIOMATIC_EXAMPLES ${BASENAMES_IDIOMATIC_EXAMPLES} ) +string( REPLACE ".cpp" "" BASENAMES_IDIOMATIC_TEST "${SOURCES_IDIOMATIC_TEST}" ) +set( TARGETS_IDIOMATIC_TEST ${BASENAMES_IDIOMATIC_TEST} ) -foreach( name ${TARGETS_IDIOMATIC_EXAMPLES} ) +foreach( name ${TARGETS_IDIOMATIC_TEST} ) add_executable(${name} ${name}.cpp) target_link_libraries(${name} jaulib catch2) add_dependencies(${name} jaulib catch2) -endforeach() - -foreach(name ${TARGETS_IDIOMATIC_EXAMPLES}) add_test (NAME ${name} COMMAND ${name}) endforeach() - +IF(TEST_WITH_SUDO) + # These examples use the standard separate compilation + file(GLOB_RECURSE SOURCES_IDIOMATIC_TESTSUDO RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "testsudo_*.cpp") + + string( REPLACE ".cpp" "" BASENAMES_IDIOMATIC_TESTSUDO "${SOURCES_IDIOMATIC_TESTSUDO}" ) + set( TARGETS_IDIOMATIC_TESTSUDO ${BASENAMES_IDIOMATIC_TESTSUDO} ) + + foreach( name ${TARGETS_IDIOMATIC_TESTSUDO} ) + add_executable(${name} ${name}.cpp) + target_link_libraries(${name} jaulib catch2) + add_dependencies(${name} jaulib catch2) + # + # cap_sys_admin is not enough to access /dev/loop* + # add_test (NAME ${name} COMMAND sudo -E /sbin/capsh --caps=cap_sys_admin+eip\ cap_setpcap,cap_setuid,cap_setgid+ep + # --keep=1 --user=$ENV{USER} --addamb=cap_sys_admin+eip + # -- -c "ulimit -c unlimited; ./${name}") + # + add_test (NAME ${name} COMMAND sudo -E ./${name}) + endforeach() +ENDIF(TEST_WITH_SUDO) diff --git a/test/testsudo_fileutils02.cpp b/test/testsudo_fileutils02.cpp new file mode 100644 index 0000000..51d8b69 --- /dev/null +++ b/test/testsudo_fileutils02.cpp @@ -0,0 +1,61 @@ +/* + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2021 Gothel Software e.K. + * + * 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_fileutils_copy_r_p.hpp" + +class TestFileUtil02 : TestFileUtilBase { + public: + + void test50_mount_copy_r_p() { + INFO_STR("\n\ntest50_mount_copy_r_p\n"); + + jau::fs::file_stats root_orig_stats(project_root1); + if( !root_orig_stats.exists() ) { + root_orig_stats = jau::fs::file_stats(project_root2); + } + REQUIRE( true == root_orig_stats.exists() ); + + std::string image_file = root_orig_stats.path() + ".sqfs"; + jau::fs::file_stats image_stats(image_file); + REQUIRE( true == image_stats.exists() ); + + const std::string mount_point = root+"_mount"; + jau::fs::remove(mount_point, jau::fs::traverse_options::recursive); // start fresh + REQUIRE( true == jau::fs::mkdir(mount_point, jau::fs::fmode_t::def_dir_prot) ); + + jau::fs::mount_ctx mctx = jau::fs::mount_image(image_stats.path(), mount_point, "squashfs", /* MS_LAZYTIME | MS_NOATIME | */ MS_RDONLY, ""); + REQUIRE( true == mctx.mounted ); + + const std::string root_copy = root+"_copy_test50"; + testxx_copy_r_p("test50_mount_copy_r_p", mount_point, 1 /* source_added_dead_links */, root_copy); + + REQUIRE( true == jau::fs::umount(mctx) ); + + if constexpr ( _remove_target_test_dir ) { + REQUIRE( true == jau::fs::remove(mount_point, jau::fs::traverse_options::recursive) ); + } + } +}; + +METHOD_AS_TEST_CASE( TestFileUtil02::test50_mount_copy_r_p, "Test TestFileUtil02 - test50_mount_copy_r_p"); diff --git a/test_data.sqfs b/test_data.sqfs Binary files differnew file mode 100644 index 0000000..c18ed31 --- /dev/null +++ b/test_data.sqfs |