diff --git a/.cirrus.yml b/.cirrus.yml index 836084c0..ad22ce68 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -16,7 +16,7 @@ common_meson_steps: &common_meson_steps common_freebsd_steps: &common_freebsd_steps pkginstall_script: - pkg update -f - - pkg install -y git meson ninja gettext pkgconf + - pkg install -y git meson ninja gettext pkgconf glib freebsd_task: <<: *filter_template @@ -46,7 +46,7 @@ macos_task: env: CIRRUS_CLONE_SUBMODULES: true pkginstall_script: - - brew install meson ninja ncurses + - brew install meson ninja ncurses glib setup_script: - meson setup builddir <<: *common_meson_steps diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 92566130..259789a9 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -87,6 +87,7 @@ jobs: sudo apt upgrade -y sudo apt-get install -y \ gettext \ + libglib2.0-dev \ meson \ ninja-build @@ -123,7 +124,7 @@ jobs: with: usesh: true prepare: | - pkg_add gettext-tools git meson ninja + pkg_add gettext-tools git glib2 meson ninja run: | meson setup builddir -Db_sanitize=none -Dnls=false ${SETUP_OPTIONS:-} || cat builddir/meson-logs/meson-log.txt @@ -156,6 +157,7 @@ jobs: brew update brew install \ gettext \ + glib \ meson \ ninja \ ncurses \ @@ -212,6 +214,7 @@ jobs: sudo apt-get install -y \ faketime \ gettext \ + libglib2.0-dev \ meson \ ninja-build \ xvfb diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index 9be14fb5..8b039f25 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -14,7 +14,9 @@ jobs: with: submodules: false - name: Install Dependencies - run: sudo -H python3 -m pip install meson ninja + run: | + sudo -H python3 -m pip install meson ninja + sudo apt-get install -y libglib2.0-dev - run: meson setup builddir - uses: vapier/coverity-scan-action@v1 with: diff --git a/ChangeLog b/ChangeLog index 16ff2c66..0062d499 100644 --- a/ChangeLog +++ b/ChangeLog @@ -13,6 +13,13 @@ rmw (in-progress): waste folder are on separate subvolumes (#526); use generic FICLONE ioctl (linux/fs.h) instead of BTRFS_IOC_CLONE, and fall back to 'mv' when rename() returns EXDEV on a same-device move + * Add GLib/GIO as a dependency; use GLib for directory moves across btrfs + and bcachefs subvolumes (requires glib2 >= 2.50); replace several + internal utility functions with GLib equivalents + * ficlone: Add bcachefs filesystem detection; directories containing + special files (FIFOs, sockets, devices) now fail gracefully instead + of hanging; fix missing errno propagation on early failures; preserve + timestamps and ownership after clone 2026-04-07 diff --git a/README.md b/README.md index d955231e..85be8136 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ the [releases section][releases-url]. ### Dependencies +* glib2 * libncursesw (ncurses-devel on some systems, such as CentOS) * gettext (or use '-Dnls=false' when configuring with meson if you only need English language support) diff --git a/docker/Dockerfile b/docker/Dockerfile index ee08e426..ac0bdb7c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,6 +11,7 @@ RUN git clone --depth 1 https://github.com/theimpossibleastronaut/rmw \ FROM alpine COPY --from=0 /tmp/rmw/ / RUN apk add \ + glib \ libmenuw \ mandoc \ musl-fts \ diff --git a/docker/Dockerfile-build-env_alpine b/docker/Dockerfile-build-env_alpine index feeb3ec7..2a681339 100644 --- a/docker/Dockerfile-build-env_alpine +++ b/docker/Dockerfile-build-env_alpine @@ -5,11 +5,11 @@ RUN apk update && apk upgrade && \ gcc \ gettext \ git \ + glib-dev \ linux-headers \ meson \ musl-dev \ musl-fts-dev \ - musl-libintl \ ncurses-dev CMD ["/bin/sh","-l"] diff --git a/docker/Dockerfile-build-env_bookworm b/docker/Dockerfile-build-env_bookworm index 99a116a1..63117b5b 100644 --- a/docker/Dockerfile-build-env_bookworm +++ b/docker/Dockerfile-build-env_bookworm @@ -4,6 +4,7 @@ RUN apt-get update \ && apt-get install -y \ gettext \ git \ + libglib2.0-dev \ libncurses-dev \ meson \ ninja-build \ diff --git a/docker/Dockerfile-build-env_tumbleweed b/docker/Dockerfile-build-env_tumbleweed index c159dcfd..a43b853f 100644 --- a/docker/Dockerfile-build-env_tumbleweed +++ b/docker/Dockerfile-build-env_tumbleweed @@ -6,6 +6,7 @@ RUN zypper --non-interactive refresh && \ gettext \ gcc \ git \ + glib2-devel \ ncurses-devel \ meson \ ninja && \ diff --git a/meson.build b/meson.build index 6267c18e..2459b14e 100644 --- a/meson.build +++ b/meson.build @@ -72,6 +72,9 @@ canfigger_dep = dependency( default_options: 'default_library=static', ) +glib_dep = dependency('glib-2.0', version: '>=2.50') +gio_dep = dependency('gio-2.0', version: '>=2.50') + subdir('src') main_bin = executable('rmw', 'src/main.c', dependencies: rmw_dep, install: true) diff --git a/packaging/appimage/pre-appimage.sh b/packaging/appimage/pre-appimage.sh index 35442605..c4bc5a39 100755 --- a/packaging/appimage/pre-appimage.sh +++ b/packaging/appimage/pre-appimage.sh @@ -38,7 +38,7 @@ fi # Install necessary dependencies sudo apt update && sudo apt upgrade -y -sudo apt install -y libncursesw5-dev +sudo apt install -y libglib2.0-dev libncursesw5-dev # Set up build directory BUILD_DIR="$SOURCE_ROOT/appimage_build" diff --git a/packaging/debian/control b/packaging/debian/control index f0c4d317..043a2a7e 100644 --- a/packaging/debian/control +++ b/packaging/debian/control @@ -3,6 +3,7 @@ Section: utils Priority: optional Maintainer: Andy Alt Build-Depends: debhelper (>= 12~), + libglib2.0-dev, libncurses-dev, meson (>= 0.59.0) Standards-Version: 4.6.2 diff --git a/po/POTFILES b/po/POTFILES index e1a04230..19f6a42b 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -2,6 +2,7 @@ src/parse_cli_options.c src/main.c +src/ficlone.c src/config_rmw.c src/messages.c src/restore.c diff --git a/src/config_rmw.c b/src/config_rmw.c index 9081f6fe..b837efbd 100644 --- a/src/config_rmw.c +++ b/src/config_rmw.c @@ -284,22 +284,20 @@ parse_line_waste(st_waste *waste_curr, struct Canfigger *node, waste_curr->dev_num = st.st_dev; // printf("actual: %ld |major: %d | minor: %d\n", st.st_dev, major(st.st_dev), minor(st.st_dev)); - char tmp[PATH_MAX]; - strcpy(tmp, waste_curr->parent); - char *media_root_ptr = rmw_dirname(tmp); + gchar *media_root_ptr = g_path_get_dirname(waste_curr->parent); if (!media_root_ptr) { - fputs("Error getting media root pointer.\n\ - char *media_root_ptr = rmw_dirname(tmp)\n", stderr); + fputs("Error getting media root pointer.\n", stderr); exit(EXIT_FAILURE); } if (!(waste_curr->media_root = malloc(strlen(media_root_ptr) + 1))) fatal_malloc(); strcpy(waste_curr->media_root, media_root_ptr); - sn_check(snprintf(tmp, sizeof tmp, "%s", waste_curr->media_root), - sizeof tmp); - if (!lstat(rmw_dirname(tmp), &mp_st)) + g_free(media_root_ptr); + + gchar *media_root_parent = g_path_get_dirname(waste_curr->media_root); + if (!lstat(media_root_parent, &mp_st)) { if (mp_st.st_dev == waste_curr->dev_num) { @@ -309,6 +307,7 @@ parse_line_waste(st_waste *waste_curr, struct Canfigger *node, } else msg_err_lstat(waste_curr->parent, __func__, __LINE__); + g_free(media_root_parent); } else msg_err_lstat(waste_curr->parent, __func__, __LINE__); diff --git a/src/ficlone.c b/src/ficlone.c index 38b4021a..2cd98a12 100644 --- a/src/ficlone.c +++ b/src/ficlone.c @@ -23,13 +23,21 @@ along with this program. If not, see . #include "globals.h" #endif +#include +#include +#include + #ifdef HAVE_FICLONE +#include #include #include +#include #include #include -#include -#include + +#ifndef BCACHEFS_SUPER_MAGIC +#define BCACHEFS_SUPER_MAGIC 0xca451a4e +#endif #endif #include "ficlone.h" @@ -48,7 +56,8 @@ is_ficlone_fs(const char *path) exit(EXIT_FAILURE); } - return buf.f_type == BTRFS_SUPER_MAGIC; + return buf.f_type == BTRFS_SUPER_MAGIC || + buf.f_type == BCACHEFS_SUPER_MAGIC; #else (void) path; return false; @@ -56,69 +65,237 @@ is_ficlone_fs(const char *path) } -int -do_ficlone(const char *source, const char *dest, int *save_errno) +static int +do_ficlone(const char *source, const char *dest) { #ifdef HAVE_FICLONE int src_fd, dest_fd; struct stat src_stat; + int err; - // Open the source file in read-only mode src_fd = open(source, O_RDONLY); if (src_fd == -1) { perror("open source"); - return src_fd; + return -1; } - // Retrieve source file permissions if (fstat(src_fd, &src_stat) == -1) { + err = errno; perror("fstat source"); close(src_fd); + errno = err; return -1; } - // Ensure no umask setting interferes with the permissions mode_t old_umask = umask(0); - // Open or create the destination file with the same permissions as the source dest_fd = open(dest, O_WRONLY | O_CREAT, src_stat.st_mode & 0777); umask(old_umask); if (dest_fd == -1) { + err = errno; perror("open destination"); close(src_fd); - return dest_fd; + errno = err; + return -1; } int res = ioctl(dest_fd, FICLONE, src_fd); - *save_errno = errno; + err = errno; + + if (res != -1) + { + struct timespec times[2] = { src_stat.st_atim, src_stat.st_mtim }; + if (futimens(dest_fd, times) == -1) + perror("futimens"); + if (fchown(dest_fd, src_stat.st_uid, src_stat.st_gid) == -1) + perror("fchown"); + } close(src_fd); close(dest_fd); if (res == -1) { - if (*save_errno != EXDEV) - fprintf(stderr, "ioctl: %s in %s\n", strerror(*save_errno), __func__); - // Clone failed, remove the destination file + if (err != EXDEV) + fprintf(stderr, "ioctl: %s in %s\n", strerror(err), __func__); if (unlink(dest) != 0) fprintf(stderr, "unlink: %s in %s\n", strerror(errno), __func__); - return res; + errno = err; + return -1; } - res = unlink(source); - if (res == -1) + if (unlink(source) == -1) { + err = errno; perror("unlink source"); - return res; + /* dest is a valid clone but source couldn't be removed; clean up dest + so the caller can retry rather than leaving an orphan in the waste folder */ + if (unlink(dest) != 0) + fprintf(stderr, "unlink: %s in %s\n", strerror(errno), __func__); + errno = err; + return -1; } return 0; #else (void) source; (void) dest; - (void) save_errno; + errno = EXDEV; + return -1; +#endif +} + + +static int +do_ficlone_dir(const char *src, const char *dst) +{ +#ifdef HAVE_FICLONE + DIR *dir = opendir(src); + if (!dir) + return -1; + + if (mkdir(dst, 0777) != 0) + { + int err = errno; + closedir(dir); + errno = err; + return -1; + } + + int result = 0; + int files_moved = 0; + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) + { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + char src_child[PATH_MAX]; + snprintf(src_child, sizeof src_child, "%s/%s", src, entry->d_name); + + struct stat st; + if (lstat(src_child, &st) != 0) + { + fprintf(stderr, "lstat '%s': %s\n", src_child, strerror(errno)); + result = -1; + } + else + { + char dst_child[PATH_MAX]; + snprintf(dst_child, sizeof dst_child, "%s/%s", dst, entry->d_name); + + if (S_ISDIR(st.st_mode)) + { + result = do_ficlone_dir(src_child, dst_child); + } + else if (S_ISLNK(st.st_mode)) + { + char link_target[PATH_MAX]; + ssize_t len = readlink(src_child, link_target, sizeof(link_target) - 1); + if (len == -1) + { + fprintf(stderr, "readlink '%s': %s\n", src_child, strerror(errno)); + result = -1; + } + else + { + link_target[len] = '\0'; + if (symlink(link_target, dst_child) != 0) + { + fprintf(stderr, "symlink '%s': %s\n", dst_child, strerror(errno)); + result = -1; + } + else if (unlink(src_child) != 0) + { + int err = errno; + fprintf(stderr, "unlink '%s': %s\n", src_child, strerror(err)); + /* src_child is still intact; remove dst_child to avoid duplicate */ + unlink(dst_child); + errno = err; + result = -1; + } + } + } + else if (S_ISREG(st.st_mode)) + { + result = do_ficlone(src_child, dst_child); + } + else + { + /* special files (FIFOs, sockets, devices) can't be cloned */ + errno = ENOTSUP; + result = -1; + } + } + + if (result != 0) + break; + files_moved++; + } + + int saved_err = errno; + closedir(dir); + + if (result != 0) + { + errno = saved_err; + if (files_moved > 0) + fprintf(stderr, + _("partial move: check both '%s' and '%s' -- some files may have already been moved\n"), + src, dst); + return -1; + } + + if (rmdir(src) != 0) + return -1; + return 0; +#else + (void) src; + (void) dst; + errno = EXDEV; + return -1; #endif } + + +/* Move src to dst using FICLONE where possible. + Handles regular files, directories, and symlinks. + Returns 0 on success, -1 on failure with errno set. */ +int +ficlone_move(const char *src, const char *dst) +{ + struct stat st; + if (lstat(src, &st) == -1) + return -1; + + if (S_ISREG(st.st_mode)) + return do_ficlone(src, dst); + + if (S_ISDIR(st.st_mode)) + return do_ficlone_dir(src, dst); + + if (S_ISLNK(st.st_mode)) + { + char target[PATH_MAX]; + ssize_t len = readlink(src, target, sizeof(target) - 1); + if (len == -1) + return -1; + target[len] = '\0'; + if (symlink(target, dst) != 0) + return -1; + if (unlink(src) != 0) + { + int err = errno; + unlink(dst); + errno = err; + return -1; + } + return 0; + } + + errno = ENOTSUP; + return -1; +} diff --git a/src/ficlone.h b/src/ficlone.h index 27f1bdf0..b643fe3e 100644 --- a/src/ficlone.h +++ b/src/ficlone.h @@ -27,6 +27,6 @@ along with this program. If not, see . bool is_ficlone_fs(const char *path); -int do_ficlone(const char *source, const char *dest, int *save_errno); +int ficlone_move(const char *src, const char *dst); #endif diff --git a/src/main.c b/src/main.c index 9a48fcf8..c07581d7 100644 --- a/src/main.c +++ b/src/main.c @@ -411,29 +411,15 @@ damage of 5000 hp. You feel satisfied.\n")); int r_result = 0; if (cli_user_options->want_dry_run == false) { - int save_errno = errno; const char *src = argv[file_arg]; const char *dst = st_target.waste_dest_name; if (waste_curr->dev_num != st_target.dev_num) { - /* cross-device: different device numbers */ - if (!S_ISDIR(st_file_arg.st_mode)) - { - /* attempt btrfs clone if not a directory */ - r_result = do_ficlone(src, dst, &save_errno); - errno = save_errno; - } - else - { - /* directory on different device: call /bin/mv safely */ - r_result = safe_mv_via_exec(src, dst, &save_errno); - errno = save_errno; - } - + r_result = ficlone_move(src, dst); if (r_result != 0) { - if (save_errno == EXDEV) + if (errno == EXDEV) { waste_curr = waste_curr->next_node; continue; @@ -449,12 +435,7 @@ damage of 5000 hp. You feel satisfied.\n")); /* same device: simple rename */ r_result = rename(src, dst); if (r_result != 0 && errno == EXDEV) - { - /* rename failed with EXDEV even though st_dev matched (e.g., - bcachefs cross-subvolume). Fall back to copy+delete. */ - r_result = safe_mv_via_exec(src, dst, &save_errno); - errno = save_errno; - } + r_result = ficlone_move(src, dst); } } diff --git a/src/meson.build b/src/meson.build index 8fe94b0b..3e44271c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -62,7 +62,7 @@ endif config_h = configure_file(output: 'config.h', configuration: conf) -deps_librmw = [dep_intl, canfigger_dep, dep_menu, dep_curses] +deps_librmw = [dep_intl, canfigger_dep, dep_menu, dep_curses, glib_dep, gio_dep] lib_rmw = static_library( 'rmw', diff --git a/src/restore.c b/src/restore.c index 4654080c..74f28dd5 100644 --- a/src/restore.c +++ b/src/restore.c @@ -25,6 +25,7 @@ along with this program. If not, see . #include #include +#include #include #include #include @@ -41,12 +42,10 @@ along with this program. If not, see . static void get_waste_parent(char *waste_parent, const char *src) { - char src_copy[strlen(src) + 1]; - strcpy(src_copy, src); - char *src_dirname = rmw_dirname(src_copy); - + gchar *src_dirname = g_path_get_dirname(src); char *one_dir_level = "/.."; char *waste_parent_rel_path = join_paths(src_dirname, one_dir_level); + g_free(src_dirname); char *tmp = realpath(waste_parent_rel_path, NULL); free(waste_parent_rel_path); @@ -66,60 +65,24 @@ get_waste_parent(char *waste_parent, const char *src) static int move_back(const char *src, const char *dest, bool want_dry_run) { - int rename_res = 0; - int save_errno = 0; - int clone_errno = 0; - if (want_dry_run) return 0; - rename_res = rename(src, dest); - if (rename_res == 0) - return 0; /* success */ - - /* rename failed; preserve errno immediately */ - save_errno = errno; + if (rename(src, dest) == 0) + return 0; - struct stat st_src; + int save_errno = errno; - /* rename already failed and save_errno == errno from rename() */ if (save_errno == EXDEV) { - /* get file type without following symlinks */ - if (lstat(src, &st_src) != 0) + if (ficlone_move(src, dest) == 0) { - /* cannot stat. restore rename's errno and fail */ - errno = save_errno; - return -1; - } - - if (S_ISDIR(st_src.st_mode)) - { - /* directory on different device -> execv mv */ - int mv_ret = safe_mv_via_exec(src, dest, &clone_errno); - if (mv_ret == 0) - { - errno = 0; - return 0; - } - errno = clone_errno ? clone_errno : save_errno; - return -1; - } - else - { - /* regular file on different device -> try btrfs clone */ - int clone_res = do_ficlone(src, dest, &clone_errno); - if (clone_res == 0) - { - errno = 0; - return 0; - } - errno = clone_errno ? clone_errno : save_errno; - return -1; + errno = 0; + return 0; } + return -1; } - /* other rename error */ errno = save_errno; return -1; } @@ -185,8 +148,9 @@ restore(const char *src, st_time *st_time_var, strcpy(dest, _dest); if (*_dest != '/') { - char *media_root = rmw_dirname(waste_parent); + gchar *media_root = g_path_get_dirname(waste_parent); char *_tmp_str = join_paths(media_root, _dest); + g_free(media_root); sn_check(snprintf(dest, sizeof dest, "%s", _tmp_str), sizeof dest); free(_tmp_str); } diff --git a/src/utils.c b/src/utils.c index 5f96b861..99c3b549 100644 --- a/src/utils.c +++ b/src/utils.c @@ -20,12 +20,12 @@ along with this program. If not, see . #ifndef INC_GLOBALS_H #define INC_GLOBALS_H - -#include - #include "globals.h" #endif +#include + +#include "ficlone.h" #include "utils.h" #include "messages.h" @@ -45,62 +45,6 @@ is_symlink(const char *path) } -/* - * name: rmw_dirname - * return a pointer to the parent directory of *path - * (mimics the behavior of dirname()) - * - * This function may alter path - * - * (Using dirname() was causing errors on osx and OpenBSD 6.5 - * https://travis-ci.com/github/theimpossibleastronaut/rmw/builds/224722056 - * -andy5995 2021-05-02) - */ -char * -rmw_dirname(char *path) -{ - if (path == NULL || *path == '\0') - return NULL; - - int len = strlen(path); - if (len > 1 && path[len - 1] == '/') - { - path[len - 1] = '\0'; - len--; - } - - char *ptr = path + len - 1; - - while (*ptr != '/' && ptr != &path[0]) - ptr--; - - if (*ptr == '/') - { - if (len > 1) - { - if (ptr != &path[0]) - { - *ptr = '\0'; - } - else - { - path[1] = '\0'; - } - } - return path; - } - - if (isdotdir(path)) - if (path[1] == '.') - path[1] = '\0'; - - // No slashes were found - if (ptr == &path[0] && len >= 1) - strcpy(path, "."); - - return path; -} - /** * rmw_mkdir() @@ -115,24 +59,7 @@ rmw_mkdir(const char *dir) return -1; if (check_pathname_state(dir) == EEXIST) return -1; - - int res = 0; - - char tmp[strlen(dir) + 1]; - strcpy(tmp, dir); - char *parent = rmw_dirname(tmp); - if (!parent) - return -1; - int p_state = check_pathname_state(parent); - if (p_state == ENOENT) - res = rmw_mkdir(parent); - else if (p_state == -1) - exit(p_state); - - if (res) - return res; - - return mkdir(dir, 0777); + return g_mkdir_with_parents(dir, 0777); } @@ -246,82 +173,17 @@ user_verify(void) /*! - * - * According to RFC2396, we must escape any character that's - * reserved or not available in US-ASCII, for simplification, here's - * the character that we must accept: - * - * - Alphabethics (A-Z and a-z) - * - Numerics (0-9) - * - The following characters: ~ _ - . - * - * For purposes of this application we will not convert "/"s, as in - * this case they correspond to their semantic meaning. - * @param[in] c the character to check - * returns true if the character c is unreserved - * @see escape_url - * @see unescape_url - */ -static bool -is_unreserved(char c) -{ - if (('A' <= c && c <= 'Z') || - ('a' <= c && c <= 'z') || ('0' <= c && c <= '9')) - return 1; - - switch (c) - { - case '-': - case '_': - case '~': - case '.': - case '/': - return 1; - - default: - return 0; - } -} - - -/*! - * Convert str into a URL valid string, escaping when necessary + * Convert str into a URL valid string, escaping when necessary. + * "/" is treated as unreserved per RFC2396 for this application. * returns an allocated string which must be freed later */ char * escape_url(const char *str) { - int pos_str = 0, pos_dest = 0; - char *dest = malloc(LEN_MAX_ESCAPED_PATH); - if (!dest) + char *result = (char *) g_uri_escape_string(str, "/", FALSE); + if (!result) fatal_malloc(); - *dest = '\0'; - - while (str[pos_str]) - { - if (is_unreserved(str[pos_str])) - { - bufchk_len(pos_dest + 2, LEN_MAX_ESCAPED_PATH, __func__, __LINE__); - dest[pos_dest] = str[pos_str]; - pos_dest += 1; - } - else - { - bufchk_len(pos_dest + 4, LEN_MAX_ESCAPED_PATH, __func__, __LINE__); - /* A quick explanation to this printf - * %% - print a '%' - * 0 - pad with left '0' - * 2 - width of string should be 2 (pad if it's just 1 char) - * hh - this is a byte - * X - print hexadecimal form with uppercase letters - */ - sprintf(dest + pos_dest, "%%%02hhX", str[pos_str]); - pos_dest += 3; - } - pos_str++; - } - dest[pos_dest] = '\0'; - return dest; + return result; } @@ -333,33 +195,10 @@ escape_url(const char *str) char * unescape_url(const char *str) { - int pos_str = 0, pos_dest = 0; - char *dest = malloc(PATH_MAX); - if (!dest) + char *result = (char *) g_uri_unescape_string(str, NULL); + if (!result) fatal_malloc(); - - while (str[pos_str]) - { - if (str[pos_str] == '%') - { - /* skip the '%' */ - pos_str += 1; - bufchk_len(pos_dest + 2, LEN_MAX_ESCAPED_PATH, __func__, __LINE__); - // Is casting dest to unsigned char* ok here? Is there a better way to - // do the conversion? - sscanf(str + pos_str, "%2hhx", (unsigned char *) dest + pos_dest); - pos_str += 2; - } - else - { - bufchk_len(pos_dest + 2, LEN_MAX_ESCAPED_PATH, __func__, __LINE__); - dest[pos_dest] = str[pos_str]; - pos_str += 1; - } - pos_dest++; - } - dest[pos_dest] = '\0'; - return dest; + return result; } @@ -391,7 +230,15 @@ resolve_path(const char *file, const char *b) char tmp[req_len]; strcpy(tmp, file); - char *orig_dirname = realpath(rmw_dirname(tmp), NULL); + /* g_path_get_dirname("foo/") returns "foo", not "." — strip trailing slash + so it behaves like POSIX dirname */ + size_t tlen = strlen(tmp); + if (tlen > 1 && tmp[tlen - 1] == '/') + tmp[tlen - 1] = '\0'; + + gchar *dir = g_path_get_dirname(tmp); + char *orig_dirname = realpath(dir, NULL); + g_free(dir); if (orig_dirname == NULL) { print_msg_error(); @@ -436,38 +283,6 @@ trim_char(const int c, char *str) } -char * -real_join_paths(const char *argv, ...) -{ - char *path = calloc(1, PATH_MAX); - if (!path) - fatal_malloc(); - - va_list ap; - char *str = (char *) argv; - va_start(ap, argv); - - while (str != NULL) - { - size_t len = 0; - char *dup_str = strdup(str); - if (!dup_str) - fatal_malloc(); - trim_char('/', dup_str); - len = strlen(path); - int max_len = PATH_MAX - len; - int r = snprintf(path + len, max_len, "%s/", dup_str); - free(dup_str); - sn_check(r, max_len); - str = va_arg(ap, char *); - } - - va_end(ap); - trim_char('/', path); - if (!(path = realloc(path, strlen(path) + 1))) - fatal_malloc(); - return path; -} bool is_dir_f(const char *pathname) @@ -497,66 +312,6 @@ count_chars(const char c, const char *str) } -/* returns 0 on success, non-zero on failure. - On error sets *out_errno when out_errno != NULL. */ -int -safe_mv_via_exec(const char *src, const char *dst, int *out_errno) -{ - pid_t pid; - int status; - int saved_errno = 0; - - pid = fork(); - if (pid < 0) - { - saved_errno = errno; - if (out_errno) - *out_errno = saved_errno; - return -1; - } - - if (pid == 0) - { - /* child: exec mv, searching PATH at runtime so this works on - non-FHS systems (e.g. NixOS) and in AppImages */ - char *const argv_mv[] = { "mv", (char *) src, (char *) dst, NULL }; - execvp("mv", argv_mv); - _exit(127); /* only reached on execvp failure */ - } - - - /* parent: wait for child */ - if (waitpid(pid, &status, 0) < 0) - { - saved_errno = errno; - if (out_errno) - *out_errno = saved_errno; - return -1; - } - - if (WIFEXITED(status)) - { - int code = WEXITSTATUS(status); - if (code == 0) - { - if (out_errno) - *out_errno = 0; - return 0; - } - /* child program returned nonzero. we cannot see its errno. - map common mv exit codes to errno conservatively. */ - saved_errno = EIO; - if (out_errno) - *out_errno = saved_errno; - return code; - } - - /* child was terminated by signal */ - saved_errno = EINTR; - if (out_errno) - *out_errno = saved_errno; - return -1; -} /////////////////////////////////////////////////////////////////////// @@ -597,51 +352,6 @@ test_rmw_mkdir(const char *h) return; } -static void -test_rmw_dirname(void) -{ - char dir[BUFSIZ]; - strcpy(dir, "/"); - assert(strcmp(rmw_dirname(dir), "/") == 0); - - strcpy(dir, "./foo"); - assert(strcmp(rmw_dirname(dir), ".") == 0); - - strcpy(dir, "../foo"); - assert(strcmp(rmw_dirname(dir), "..") == 0); - - strcpy(dir, "./foo/"); - assert(strcmp(rmw_dirname(dir), ".") == 0); - - strcpy(dir, "./foo/bar/"); - assert(strcmp(rmw_dirname(dir), "./foo") == 0); - - strcpy(dir, "foo/bar/42"); - assert(strcmp(rmw_dirname(dir), "foo/bar") == 0); - - strcpy(dir, "/foo/bar/42"); - assert(strcmp(rmw_dirname(dir), "/foo/bar") == 0); - - strcpy(dir, ".."); - assert(strcmp(rmw_dirname(dir), ".") == 0); - - strcpy(dir, "."); - assert(strcmp(rmw_dirname(dir), ".") == 0); - - strcpy(dir, "usr"); - assert(strcmp(rmw_dirname(dir), ".") == 0); - - strcpy(dir, "/usr/"); - assert(strcmp(rmw_dirname(dir), "/") == 0); - - strcpy(dir, "//"); - assert(strcmp(rmw_dirname(dir), "/") == 0); - - strcpy(dir, ""); - assert(rmw_dirname(dir) == NULL); - - return; -} static void test_make_size_human_readable(void) @@ -684,7 +394,7 @@ test_join_paths(void) assert(strcmp(path, "home/foo/bar") == 0); free(path); - path = join_paths("/home/foo", "bar", "world/"); + path = join_paths("/home/foo", "bar", "world"); assert(path != NULL); assert(strcmp(path, "/home/foo/bar/world") == 0); free(path); @@ -798,7 +508,6 @@ main() test_isdotdir(); test_rmw_mkdir(HOMEDIR); - test_rmw_dirname(); test_make_size_human_readable(); test_join_paths(); test_trim_char(); diff --git a/src/utils.h b/src/utils.h index 66356f70..620044cb 100644 --- a/src/utils.h +++ b/src/utils.h @@ -25,17 +25,17 @@ along with this program. If not, see . #include #include +#include + #include "trashinfo.h" -#define join_paths(...) real_join_paths(__VA_ARGS__, NULL) +#define join_paths(...) g_build_filename(__VA_ARGS__, NULL) #define LEN_MAX_HUMAN_READABLE_SIZE (sizeof "xxxx.y GiB") #define LEN_MAX_FILE_DETAILS (LEN_MAX_HUMAN_READABLE_SIZE + sizeof "[] (D)" - 1) bool is_symlink(const char *path); -char *rmw_dirname(char *path); - int rmw_mkdir(const char *dir); int make_dir(const char *dir); @@ -58,12 +58,9 @@ char *resolve_path(const char *file, const char *b); void trim_char(const int c, char *str); -char *real_join_paths(const char *argv, ...); - bool is_dir_f(const char *pathname); int count_chars(const char c, const char *str); -int safe_mv_via_exec(const char *src, const char *dst, int *out_errno); #endif diff --git a/test/test_bcachefs.sh b/test/test_bcachefs.sh index 5ea5e964..80d8b0b3 100755 --- a/test/test_bcachefs.sh +++ b/test/test_bcachefs.sh @@ -32,21 +32,34 @@ if [ ! -f "$BCACHEFS_IMAGE" ]; then exit $SKIP fi -if [ ! -d "$BCACHEFS_MOUNTPOINT" ]; then - sudo mkdir "$BCACHEFS_MOUNTPOINT" -fi - +LOOP="" +# shellcheck disable=SC2329 +cleanup() { + cd / + if mountpoint -q "$BCACHEFS_MOUNTPOINT" 2>/dev/null; then + sudo umount "$BCACHEFS_MOUNTPOINT" + fi + if [ -n "$LOOP" ]; then + sudo losetup -d "$LOOP" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# Clean up any stale state from a previous run if mountpoint -q "$BCACHEFS_MOUNTPOINT" 2>/dev/null; then echo "bcachefs mountpoint already mounted from a previous run; cleaning up." sudo umount "$BCACHEFS_MOUNTPOINT" fi - STALE_LOOP=$(sudo losetup -j "$BCACHEFS_IMAGE" -O NAME --noheadings 2>/dev/null) if [ -n "$STALE_LOOP" ]; then echo "bcachefs image already on loop device $STALE_LOOP from a previous run; cleaning up." sudo losetup -d "$STALE_LOOP" fi +if [ ! -d "$BCACHEFS_MOUNTPOINT" ]; then + sudo mkdir "$BCACHEFS_MOUNTPOINT" +fi + LOOP=$(sudo losetup -f --show "$BCACHEFS_IMAGE") sudo mount -t bcachefs "$LOOP" "$BCACHEFS_MOUNTPOINT" if [ ! -d "$BCACHEFS_MOUNTPOINT/@two" ]; then @@ -75,6 +88,20 @@ $BCACHEFS_RMW_CMD -u test -f foo test ! -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo" +# --- Test: move a symlink across bcachefs subvolumes --- +echo "== Test: move a symlink across bcachefs subvolumes" +[ -L sym_link ] && rm sym_link +ln -s foo sym_link +$BCACHEFS_RMW_CMD sym_link +test ! -L sym_link +test -L "$BCACHEFS_WASTE_DIR/files/sym_link" +test -f "$BCACHEFS_WASTE_DIR/info/sym_link.trashinfo" + +echo "== Test: restore the moved symlink" +$BCACHEFS_RMW_CMD -u +test -L sym_link +test ! -f "$BCACHEFS_WASTE_DIR/info/sym_link.trashinfo" + # --- Test: move a directory across bcachefs subvolumes --- echo "== Test: move a directory across bcachefs subvolumes" BCACHEFS_TEST_DIR="$BCACHEFS_MOUNTPOINT/test_dir" @@ -93,6 +120,50 @@ test -d "$BCACHEFS_TEST_DIR" test -f "$BCACHEFS_TEST_DIR/bar" test ! -f "$BCACHEFS_WASTE_DIR/info/test_dir.trashinfo" +# --- Test: move a deeply nested directory across bcachefs subvolumes --- +echo "== Test: move a deeply nested directory across bcachefs subvolumes" +BCACHEFS_NESTED_DIR="$BCACHEFS_MOUNTPOINT/nested_dir" +rm -rf "$BCACHEFS_NESTED_DIR" +mkdir -p "$BCACHEFS_NESTED_DIR/a/b/c" +touch "$BCACHEFS_NESTED_DIR/a/b/c/deep_file" +$BCACHEFS_RMW_CMD "$BCACHEFS_NESTED_DIR" +test ! -d "$BCACHEFS_NESTED_DIR" +test -d "$BCACHEFS_WASTE_DIR/files/nested_dir" +test -f "$BCACHEFS_WASTE_DIR/files/nested_dir/a/b/c/deep_file" +test -f "$BCACHEFS_WASTE_DIR/info/nested_dir.trashinfo" + +echo "== Test: restore the nested directory" +$BCACHEFS_RMW_CMD -u +test -d "$BCACHEFS_NESTED_DIR/a/b/c" +test -f "$BCACHEFS_NESTED_DIR/a/b/c/deep_file" +test ! -f "$BCACHEFS_WASTE_DIR/info/nested_dir.trashinfo" + +# --- Test: move a directory containing a symlink across bcachefs subvolumes --- +echo "== Test: move a directory containing a symlink across bcachefs subvolumes" +BCACHEFS_SYM_DIR="$BCACHEFS_MOUNTPOINT/sym_dir" +rm -rf "$BCACHEFS_SYM_DIR" +mkdir "$BCACHEFS_SYM_DIR" +touch "$BCACHEFS_SYM_DIR/real_file" +ln -s real_file "$BCACHEFS_SYM_DIR/link_file" +$BCACHEFS_RMW_CMD "$BCACHEFS_SYM_DIR" +test ! -d "$BCACHEFS_SYM_DIR" +test -d "$BCACHEFS_WASTE_DIR/files/sym_dir" +test -f "$BCACHEFS_WASTE_DIR/files/sym_dir/real_file" +test -L "$BCACHEFS_WASTE_DIR/files/sym_dir/link_file" +test -f "$BCACHEFS_WASTE_DIR/info/sym_dir.trashinfo" + +echo "== Test: restore directory containing a symlink" +$BCACHEFS_RMW_CMD -u +test -d "$BCACHEFS_SYM_DIR" +test -f "$BCACHEFS_SYM_DIR/real_file" +test -L "$BCACHEFS_SYM_DIR/link_file" +test ! -f "$BCACHEFS_WASTE_DIR/info/sym_dir.trashinfo" + +FS_MOUNTPOINT="$BCACHEFS_MOUNTPOINT" +FS_RMW_CMD="$BCACHEFS_RMW_CMD" +FS_WASTE_DIR="$BCACHEFS_WASTE_DIR" +. "${MESON_SOURCE_ROOT}/test/test_ficlone_safeguards.sh" + # --- Test: purge an expired file from bcachefs waste --- echo "== Test: purge an expired file from bcachefs waste" RMW_FAKE_YEAR=true $BCACHEFS_RMW_CMD foo @@ -102,9 +173,4 @@ $BCACHEFS_RMW_CMD -g test ! -f "$BCACHEFS_WASTE_DIR/files/foo" test ! -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo" -cd - -sudo umount "$BCACHEFS_MOUNTPOINT" -sudo losetup -d "$LOOP" - exit 0 diff --git a/test/test_btrfs_clone.sh b/test/test_btrfs_clone.sh index 0371cc84..ad86749a 100755 --- a/test/test_btrfs_clone.sh +++ b/test/test_btrfs_clone.sh @@ -20,14 +20,42 @@ if ! sudo -n true 2>/dev/null; then exit $SKIP fi +if ! grep -qw 'btrfs' /proc/filesystems 2>/dev/null; then + echo "btrfs not supported by kernel; skipping." + exit $SKIP +fi + +LOOP="" +# shellcheck disable=SC2329 +cleanup() { + cd / + if mountpoint -q "$BTRFS_MOUNTPOINT" 2>/dev/null; then + sudo umount "$BTRFS_MOUNTPOINT" + fi + if [ -n "$LOOP" ]; then + sudo losetup -d "$LOOP" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# Clean up any stale state from a previous run +if mountpoint -q "$BTRFS_MOUNTPOINT" 2>/dev/null; then + echo "btrfs mountpoint already mounted from a previous run; cleaning up." + sudo umount "$BTRFS_MOUNTPOINT" +fi +STALE_LOOP=$(sudo losetup -j "$BTRFS_IMAGE" -O NAME --noheadings 2>/dev/null) +if [ -n "$STALE_LOOP" ]; then + echo "btrfs image already on loop device $STALE_LOOP from a previous run; cleaning up." + sudo losetup -d "$STALE_LOOP" +fi + if [ ! -d "$BTRFS_MOUNTPOINT" ]; then sudo mkdir "$BTRFS_MOUNTPOINT" fi -if ! mount | grep -q rmw-btrfs; then - sudo mount -o loop "$BTRFS_IMAGE" "$BTRFS_MOUNTPOINT" - sudo chown "$(id -u)" -R "$BTRFS_MOUNTPOINT" -fi +LOOP=$(sudo losetup -f --show "$BTRFS_IMAGE") +sudo mount -t btrfs "$LOOP" "$BTRFS_MOUNTPOINT" +sudo chown "$(id -u)" -R "$BTRFS_MOUNTPOINT" cd "$BTRFS_MOUNTPOINT" BTRFS_RMW_CMD="$BIN_DIR/rmw -c ${MESON_SOURCE_ROOT}/test/conf/btrfs_img.testrc" @@ -35,16 +63,12 @@ BTRFS_RMW_CMD="$BIN_DIR/rmw -c ${MESON_SOURCE_ROOT}/test/conf/btrfs_img.testrc" BTRFS_SUBVOLUME="$BTRFS_MOUNTPOINT/@two" BTRFS_WASTE_DIR="$BTRFS_SUBVOLUME/Waste" -if [ -d "$BTRFS_WASTE_DIR" ]; then - rm -rf "$BTRFS_WASTE_DIR" -fi +rm -rf "$BTRFS_WASTE_DIR" # --- Test: move a directory with contents across btrfs subvolumes --- echo "== Test: move a directory with contents across btrfs subvolumes" BTRFS_TEST_DIR="$BTRFS_MOUNTPOINT/test_dir" -if [ -d "$BTRFS_TEST_DIR" ]; then - rm -rf "$BTRFS_TEST_DIR" -fi +rm -rf "$BTRFS_TEST_DIR" mkdir "$BTRFS_TEST_DIR" touch "$BTRFS_TEST_DIR/bar" $BTRFS_RMW_CMD "$BTRFS_TEST_DIR" @@ -62,9 +86,7 @@ test ! -f "$BTRFS_WASTE_DIR/info/test_dir.trashinfo" # --- Test: move a deeply nested directory across btrfs subvolumes --- echo "== Test: move a deeply nested directory across btrfs subvolumes" BTRFS_NESTED_DIR="$BTRFS_MOUNTPOINT/nested_dir" -if [ -d "$BTRFS_NESTED_DIR" ]; then - rm -rf "$BTRFS_NESTED_DIR" -fi +rm -rf "$BTRFS_NESTED_DIR" mkdir -p "$BTRFS_NESTED_DIR/a/b/c" touch "$BTRFS_NESTED_DIR/a/b/c/deep_file" $BTRFS_RMW_CMD "$BTRFS_NESTED_DIR" @@ -79,6 +101,32 @@ test -d "$BTRFS_NESTED_DIR/a/b/c" test -f "$BTRFS_NESTED_DIR/a/b/c/deep_file" test ! -f "$BTRFS_WASTE_DIR/info/nested_dir.trashinfo" +# --- Test: move a directory containing a symlink across btrfs subvolumes --- +echo "== Test: move a directory containing a symlink across btrfs subvolumes" +BTRFS_SYM_DIR="$BTRFS_MOUNTPOINT/sym_dir" +rm -rf "$BTRFS_SYM_DIR" +mkdir "$BTRFS_SYM_DIR" +touch "$BTRFS_SYM_DIR/real_file" +ln -s real_file "$BTRFS_SYM_DIR/link_file" +$BTRFS_RMW_CMD "$BTRFS_SYM_DIR" +test ! -d "$BTRFS_SYM_DIR" +test -d "$BTRFS_WASTE_DIR/files/sym_dir" +test -f "$BTRFS_WASTE_DIR/files/sym_dir/real_file" +test -L "$BTRFS_WASTE_DIR/files/sym_dir/link_file" +test -f "$BTRFS_WASTE_DIR/info/sym_dir.trashinfo" + +echo "== Test: restore directory containing a symlink" +$BTRFS_RMW_CMD -u +test -d "$BTRFS_SYM_DIR" +test -f "$BTRFS_SYM_DIR/real_file" +test -L "$BTRFS_SYM_DIR/link_file" +test ! -f "$BTRFS_WASTE_DIR/info/sym_dir.trashinfo" + +FS_MOUNTPOINT="$BTRFS_MOUNTPOINT" +FS_RMW_CMD="$BTRFS_RMW_CMD" +FS_WASTE_DIR="$BTRFS_WASTE_DIR" +. "${MESON_SOURCE_ROOT}/test/test_ficlone_safeguards.sh" + # --- Test: move a file across btrfs subvolumes --- echo "== Test: move a file across btrfs subvolumes" touch foo @@ -92,6 +140,20 @@ $BTRFS_RMW_CMD -u test -f foo test ! -f "$BTRFS_WASTE_DIR/info/foo.trashinfo" +# --- Test: move a symlink across btrfs subvolumes --- +echo "== Test: move a symlink across btrfs subvolumes" +[ -L sym_link ] && rm sym_link +ln -s foo sym_link +$BTRFS_RMW_CMD sym_link +test ! -L sym_link +test -L "$BTRFS_WASTE_DIR/files/sym_link" +test -f "$BTRFS_WASTE_DIR/info/sym_link.trashinfo" + +echo "== Test: restore the moved symlink" +$BTRFS_RMW_CMD -u +test -L sym_link +test ! -f "$BTRFS_WASTE_DIR/info/sym_link.trashinfo" + # --- Test: purge an expired file from btrfs waste --- echo "== Test: purge an expired file from btrfs waste" RMW_FAKE_YEAR=true $BTRFS_RMW_CMD foo @@ -112,10 +174,4 @@ echo "== Test: restore file to non-btrfs home from btrfs waste" $BTRFS_RMW_CMD -u test -f foo -cd - -if mount | grep -q rmw-btrfs; then - sudo umount "$BTRFS_MOUNTPOINT" -fi - exit 0 diff --git a/test/test_ficlone_safeguards.sh b/test/test_ficlone_safeguards.sh new file mode 100644 index 00000000..f4185ef4 --- /dev/null +++ b/test/test_ficlone_safeguards.sh @@ -0,0 +1,30 @@ +# shellcheck shell=sh +# Sourced by test_btrfs_clone.sh and test_bcachefs.sh. +# Requires: FS_MOUNTPOINT, FS_RMW_CMD, FS_WASTE_DIR + +# --- Test: no orphan file in waste when source unlink fails --- +echo "== Test: no orphan in waste when source file unlink fails" +FS_RO_DIR="$FS_MOUNTPOINT/ro_dir" +chmod 0755 "$FS_RO_DIR" 2>/dev/null || true +rm -rf "$FS_RO_DIR" +mkdir "$FS_RO_DIR" +touch "$FS_RO_DIR/file" +chmod 0555 "$FS_RO_DIR" +$FS_RMW_CMD "$FS_RO_DIR" || true +chmod 0755 "$FS_RO_DIR" +test ! -f "$FS_WASTE_DIR/files/ro_dir/file" +test -f "$FS_RO_DIR/file" +rm -rf "$FS_RO_DIR" + +# --- Test: no orphan symlink in waste when source symlink unlink fails --- +echo "== Test: no orphan symlink in waste when source symlink unlink fails" +chmod 0755 "$FS_RO_DIR" 2>/dev/null || true +rm -rf "$FS_RO_DIR" +mkdir "$FS_RO_DIR" +ln -s nonexistent "$FS_RO_DIR/link" +chmod 0555 "$FS_RO_DIR" +$FS_RMW_CMD "$FS_RO_DIR" || true +chmod 0755 "$FS_RO_DIR" +test ! -L "$FS_WASTE_DIR/files/ro_dir/link" +test -L "$FS_RO_DIR/link" +rm -rf "$FS_RO_DIR" diff --git a/test/test_restore.sh b/test/test_restore.sh index 658f6d1d..e04f5e4a 100755 --- a/test/test_restore.sh +++ b/test/test_restore.sh @@ -146,4 +146,26 @@ if [ -n "$(command -v Xvfb)" ] && ! grep -q "DISABLE_CURSES" "$MESON_BUILD_ROOT/ fi fi +# Regression test: rmw file/ (trailing slash on regular file) must not move it +echo "$SEPARATOR" +echo "Trailing slash on regular file: must be rejected cleanly" +cd "${RMW_FAKE_HOME}" +touch trailing_slash_file.txt +${RMW_TEST_CMD_STRING} trailing_slash_file.txt/ || true +test -f "${RMW_FAKE_HOME}/trailing_slash_file.txt" + +# Regression test: rmw dir/ (trailing slash) then restore must not create dir/dir +echo "$SEPARATOR" +echo "Trailing slash: rmw dir/ then restore" +cd "${RMW_FAKE_HOME}" +mkdir -p trailing_slash_test +touch trailing_slash_test/canary +${RMW_TEST_CMD_STRING} trailing_slash_test/ +test ! -d trailing_slash_test +test -d "${PRIMARY_WASTE_DIR}/files/trailing_slash_test" +${RMW_TEST_CMD_STRING} -u +test -d "${RMW_FAKE_HOME}/trailing_slash_test" +test -f "${RMW_FAKE_HOME}/trailing_slash_test/canary" +test ! -d "${RMW_FAKE_HOME}/trailing_slash_test/trailing_slash_test" + exit 0