chiark / gitweb /
Remove repeated 'the's
[elogind.git] / src / shared / path-util.c
index 57554cd294613eed7068c6a978edb0f01149f768..8f49d652664fcf2b63aa29740da6c61f741e350f 100644 (file)
   along with systemd; If not, see <http://www.gnu.org/licenses/>.
 ***/
 
-#include <assert.h>
 #include <string.h>
 #include <unistd.h>
 #include <errno.h>
 #include <stdlib.h>
-#include <signal.h>
 #include <stdio.h>
 #include <fcntl.h>
-#include <dirent.h>
 #include <sys/statvfs.h>
 
 #include "macro.h"
@@ -36,6 +33,7 @@
 #include "strv.h"
 #include "path-util.h"
 #include "missing.h"
+#include "fileio.h"
 
 bool path_is_absolute(const char *p) {
         return p[0] == '/';
@@ -129,7 +127,7 @@ char *path_make_absolute_cwd(const char *p) {
         if (!cwd)
                 return NULL;
 
-        return path_make_absolute(p, cwd);
+        return strjoin(cwd, "/", p, NULL);
 }
 
 int path_make_relative(const char *from_dir, const char *to_path, char **_r) {
@@ -306,6 +304,7 @@ char **path_strv_resolve(char **l, const char *prefix) {
                         } else {
                                 /* canonicalized path goes outside of
                                  * prefix, keep the original path instead */
+                                free(u);
                                 u = orig;
                                 orig = NULL;
                         }
@@ -402,12 +401,18 @@ char* path_startswith(const char *path, const char *prefix) {
         }
 }
 
-bool path_equal(const char *a, const char *b) {
+int path_compare(const char *a, const char *b) {
+        int d;
+
         assert(a);
         assert(b);
 
-        if ((a[0] == '/') != (b[0] == '/'))
-                return false;
+        /* A relative path and an abolute path must not compare as equal.
+         * Which one is sorted before the other does not really matter.
+         * Here a relative path is ordered before an absolute path. */
+        d = (a[0] == '/') - (b[0] == '/');
+        if (d)
+                return d;
 
         for (;;) {
                 size_t j, k;
@@ -416,113 +421,252 @@ bool path_equal(const char *a, const char *b) {
                 b += strspn(b, "/");
 
                 if (*a == 0 && *b == 0)
-                        return true;
+                        return 0;
 
-                if (*a == 0 || *b == 0)
-                        return false;
+                /* Order prefixes first: "/foo" before "/foo/bar" */
+                if (*a == 0)
+                        return -1;
+                if (*b == 0)
+                        return 1;
 
                 j = strcspn(a, "/");
                 k = strcspn(b, "/");
 
-                if (j != k)
-                        return false;
+                /* Alphabetical sort: "/foo/aaa" before "/foo/b" */
+                d = memcmp(a, b, MIN(j, k));
+                if (d)
+                        return (d > 0) - (d < 0); /* sign of d */
 
-                if (memcmp(a, b, j) != 0)
-                        return false;
+                /* Sort "/foo/a" before "/foo/aaa" */
+                d = (j > k) - (j < k);  /* sign of (j - k) */
+                if (d)
+                        return d;
 
                 a += j;
                 b += k;
         }
 }
 
+bool path_equal(const char *a, const char *b) {
+        return path_compare(a, b) == 0;
+}
+
+bool path_equal_or_files_same(const char *a, const char *b) {
+        return path_equal(a, b) || files_same(a, b) > 0;
+}
+
 char* path_join(const char *root, const char *path, const char *rest) {
         assert(path);
 
         if (!isempty(root))
-                return strjoin(root, "/",
+                return strjoin(root, endswith(root, "/") ? "" : "/",
                                path[0] == '/' ? path+1 : path,
-                               rest ? "/" : NULL,
+                               rest ? (endswith(path, "/") ? "" : "/") : NULL,
                                rest && rest[0] == '/' ? rest+1 : rest,
                                NULL);
         else
                 return strjoin(path,
-                               rest ? "/" : NULL,
+                               rest ? (endswith(path, "/") ? "" : "/") : NULL,
                                rest && rest[0] == '/' ? rest+1 : rest,
                                NULL);
 }
 
-int path_is_mount_point(const char *t, bool allow_symlink) {
+static int fd_fdinfo_mnt_id(int fd, const char *filename, int flags, int *mnt_id) {
+        char path[strlen("/proc/self/fdinfo/") + DECIMAL_STR_MAX(int)];
+        _cleanup_free_ char *fdinfo = NULL;
+        _cleanup_close_ int subfd = -1;
+        char *p;
+        int r;
+
+        if ((flags & AT_EMPTY_PATH) && isempty(filename))
+                xsprintf(path, "/proc/self/fdinfo/%i", fd);
+        else {
+                subfd = openat(fd, filename, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_PATH);
+                if (subfd < 0)
+                        return -errno;
+
+                xsprintf(path, "/proc/self/fdinfo/%i", subfd);
+        }
+
+        r = read_full_file(path, &fdinfo, NULL);
+        if (r == -ENOENT) /* The fdinfo directory is a relatively new addition */
+                return -EOPNOTSUPP;
+        if (r < 0)
+                return -errno;
+
+        p = startswith(fdinfo, "mnt_id:");
+        if (!p) {
+                p = strstr(fdinfo, "\nmnt_id:");
+                if (!p) /* The mnt_id field is a relatively new addition */
+                        return -EOPNOTSUPP;
 
-        union file_handle_union h = {
-                .handle.handle_bytes = MAX_HANDLE_SZ
-        };
+                p += 8;
+        }
+
+        p += strspn(p, WHITESPACE);
+        p[strcspn(p, WHITESPACE)] = 0;
+
+        return safe_atoi(p, mnt_id);
+}
 
-        int mount_id, mount_id_parent;
-        _cleanup_free_ char *parent = NULL;
+int fd_is_mount_point(int fd, const char *filename, int flags) {
+        union file_handle_union h = FILE_HANDLE_INIT, h_parent = FILE_HANDLE_INIT;
+        int mount_id = -1, mount_id_parent = -1;
+        bool nosupp = false, check_st_dev = true;
         struct stat a, b;
         int r;
 
-        /* We are not actually interested in the file handles, but
-         * name_to_handle_at() also passes us the mount ID, hence use
-         * it but throw the handle away */
+        assert(fd >= 0);
+        assert(filename);
 
-        if (path_equal(t, "/"))
-                return 1;
-
-        r = name_to_handle_at(AT_FDCWD, t, &h.handle, &mount_id, allow_symlink ? AT_SYMLINK_FOLLOW : 0);
+        /* First we will try the name_to_handle_at() syscall, which
+         * tells us the mount id and an opaque file "handle". It is
+         * not supported everywhere though (kernel compile-time
+         * option, not all file systems are hooked up). If it works
+         * the mount id is usually good enough to tell us whether
+         * something is a mount point.
+         *
+         * If that didn't work we will try to read the mount id from
+         * /proc/self/fdinfo/<fd>. This is almost as good as
+         * name_to_handle_at(), however, does not return the
+         * opaque file handle. The opaque file handle is pretty useful
+         * to detect the root directory, which we should always
+         * consider a mount point. Hence we use this only as
+         * fallback. Exporting the mnt_id in fdinfo is a pretty recent
+         * kernel addition.
+         *
+         * As last fallback we do traditional fstat() based st_dev
+         * comparisons. This is how things were traditionally done,
+         * but unionfs breaks breaks this since it exposes file
+         * systems with a variety of st_dev reported. Also, btrfs
+         * subvolumes have different st_dev, even though they aren't
+         * real mounts of their own. */
+
+        r = name_to_handle_at(fd, filename, &h.handle, &mount_id, flags);
         if (r < 0) {
-                if (IN_SET(errno, ENOSYS, EOPNOTSUPP))
+                if (errno == ENOSYS)
+                        /* This kernel does not support name_to_handle_at()
+                         * fall back to simpler logic. */
+                        goto fallback_fdinfo;
+                else if (errno == EOPNOTSUPP)
                         /* This kernel or file system does not support
-                         * name_to_handle_at(), hence fallback to the
+                         * name_to_handle_at(), hence let's see if the
+                         * upper fs supports it (in which case it is a
+                         * mount point), otherwise fallback to the
                          * traditional stat() logic */
-                        goto fallback;
-
-                if (errno == ENOENT)
-                        return 0;
+                        nosupp = true;
+                else
+                        return -errno;
+        }
 
-                return -errno;
+        r = name_to_handle_at(fd, "", &h_parent.handle, &mount_id_parent, AT_EMPTY_PATH);
+        if (r < 0) {
+                if (errno == EOPNOTSUPP) {
+                        if (nosupp)
+                                /* Neither parent nor child do name_to_handle_at()?
+                                   We have no choice but to fall back. */
+                                goto fallback_fdinfo;
+                        else
+                                /* The parent can't do name_to_handle_at() but the
+                                 * directory we are interested in can?
+                                 * If so, it must be a mount point. */
+                                return 1;
+                } else
+                        return -errno;
         }
 
-        r = path_get_parent(t, &parent);
+        /* The parent can do name_to_handle_at() but the
+         * directory we are interested in can't? If so, it
+         * must be a mount point. */
+        if (nosupp)
+                return 1;
+
+        /* If the file handle for the directory we are
+         * interested in and its parent are identical, we
+         * assume this is the root directory, which is a mount
+         * point. */
+
+        if (h.handle.handle_bytes == h_parent.handle.handle_bytes &&
+            h.handle.handle_type == h_parent.handle.handle_type &&
+            memcmp(h.handle.f_handle, h_parent.handle.f_handle, h.handle.handle_bytes) == 0)
+                return 1;
+
+        return mount_id != mount_id_parent;
+
+fallback_fdinfo:
+        r = fd_fdinfo_mnt_id(fd, filename, flags, &mount_id);
+        if (r == -EOPNOTSUPP)
+                goto fallback_fstat;
         if (r < 0)
                 return r;
 
-        h.handle.handle_bytes = MAX_HANDLE_SZ;
-        r = name_to_handle_at(AT_FDCWD, parent, &h.handle, &mount_id_parent, 0);
-        if (r < 0) {
-                /* The parent can't do name_to_handle_at() but the
-                 * directory we are interested in can? If so, it must
-                 * be a mount point */
-                if (errno == EOPNOTSUPP)
-                        return 1;
+        r = fd_fdinfo_mnt_id(fd, "", AT_EMPTY_PATH, &mount_id_parent);
+        if (r < 0)
+                return r;
+
+        if (mount_id != mount_id_parent)
+                return 1;
 
+        /* Hmm, so, the mount ids are the same. This leaves one
+         * special case though for the root file system. For that,
+         * let's see if the parent directory has the same inode as we
+         * are interested in. Hence, let's also do fstat() checks now,
+         * too, but avoid the st_dev comparisons, since they aren't
+         * that useful on unionfs mounts. */
+        check_st_dev = false;
+
+fallback_fstat:
+        /* yay for fstatat() taking a different set of flags than the other
+         * _at() above */
+        if (flags & AT_SYMLINK_FOLLOW)
+                flags &= ~AT_SYMLINK_FOLLOW;
+        else
+                flags |= AT_SYMLINK_NOFOLLOW;
+        if (fstatat(fd, filename, &a, flags) < 0)
                 return -errno;
-        }
 
-        return mount_id != mount_id_parent;
+        if (fstatat(fd, "", &b, AT_EMPTY_PATH) < 0)
+                return -errno;
 
-fallback:
-        if (allow_symlink)
-                r = stat(t, &a);
-        else
-                r = lstat(t, &a);
+        /* A directory with same device and inode as its parent? Must
+         * be the root directory */
+        if (a.st_dev == b.st_dev &&
+            a.st_ino == b.st_ino)
+                return 1;
 
-        if (r < 0) {
-                if (errno == ENOENT)
-                        return 0;
+        return check_st_dev && (a.st_dev != b.st_dev);
+}
 
-                return -errno;
+/* flags can be AT_SYMLINK_FOLLOW or 0 */
+int path_is_mount_point(const char *t, int flags) {
+        _cleanup_close_ int fd = -1;
+        _cleanup_free_ char *canonical = NULL, *parent = NULL;
+        int r;
+
+        assert(t);
+
+        if (path_equal(t, "/"))
+                return 1;
+
+        /* we need to resolve symlinks manually, we can't just rely on
+         * fd_is_mount_point() to do that for us; if we have a structure like
+         * /bin -> /usr/bin/ and /usr is a mount point, then the parent that we
+         * look at needs to be /usr, not /. */
+        if (flags & AT_SYMLINK_FOLLOW) {
+                canonical = canonicalize_file_name(t);
+                if (!canonical)
+                        return -errno;
         }
 
-        r = path_get_parent(t, &parent);
+        r = path_get_parent(canonical ?: t, &parent);
         if (r < 0)
                 return r;
 
-        r = lstat(parent, &b);
-        if (r < 0)
+        fd = openat(AT_FDCWD, parent, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_PATH);
+        if (fd < 0)
                 return -errno;
 
-        return a.st_dev != b.st_dev;
+        return fd_is_mount_point(fd, basename(canonical ?: t), flags);
 }
 
 int path_is_read_only_fs(const char *path) {
@@ -533,7 +677,16 @@ int path_is_read_only_fs(const char *path) {
         if (statvfs(path, &st) < 0)
                 return -errno;
 
-        return !!(st.f_flag & ST_RDONLY);
+        if (st.f_flag & ST_RDONLY)
+                return true;
+
+        /* On NFS, statvfs() might not reflect whether we can actually
+         * write to the remote share. Let's try again with
+         * access(W_OK) which is more reliable, at least sometimes. */
+        if (access(path, W_OK) < 0 && errno == EROFS)
+                return true;
+
+        return false;
 }
 
 int path_is_os_tree(const char *path) {
@@ -541,24 +694,24 @@ int path_is_os_tree(const char *path) {
         int r;
 
         /* We use /usr/lib/os-release as flag file if something is an OS */
-        p = strappenda(path, "/usr/lib/os-release");
+        p = strjoina(path, "/usr/lib/os-release");
         r = access(p, F_OK);
 
         if (r >= 0)
                 return 1;
 
         /* Also check for the old location in /etc, just in case. */
-        p = strappenda(path, "/etc/os-release");
+        p = strjoina(path, "/etc/os-release");
         r = access(p, F_OK);
 
         return r >= 0;
 }
 
-int find_binary(const char *name, char **filename) {
+int find_binary(const char *name, bool local, char **filename) {
         assert(name);
 
         if (is_path(name)) {
-                if (access(name, X_OK) < 0)
+                if (local && access(name, X_OK) < 0)
                         return -errno;
 
                 if (filename) {
@@ -646,9 +799,9 @@ int fsck_exists(const char *fstype) {
         const char *checker;
         int r;
 
-        checker = strappenda("fsck.", fstype);
+        checker = strjoina("fsck.", fstype);
 
-        r = find_binary(checker, &p);
+        r = find_binary(checker, true, &p);
         if (r < 0)
                 return r;
 
@@ -664,3 +817,37 @@ int fsck_exists(const char *fstype) {
 
         return 0;
 }
+
+char *prefix_root(const char *root, const char *path) {
+        char *n, *p;
+        size_t l;
+
+        /* If root is passed, prefixes path with it. Otherwise returns
+         * it as is. */
+
+        assert(path);
+
+        /* First, drop duplicate prefixing slashes from the path */
+        while (path[0] == '/' && path[1] == '/')
+                path++;
+
+        if (isempty(root) || path_equal(root, "/"))
+                return strdup(path);
+
+        l = strlen(root) + 1 + strlen(path) + 1;
+
+        n = new(char, l);
+        if (!n)
+                return NULL;
+
+        p = stpcpy(n, root);
+
+        while (p > n && p[-1] == '/')
+                p--;
+
+        if (path[0] != '/')
+                *(p++) = '/';
+
+        strcpy(p, path);
+        return n;
+}