/*
 * Copyright (C) 2017  Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Vratislav Podzimek <vpodzime@redhat.com>
 */

#define _GNU_SOURCE
#include <unistd.h>

#include <glib.h>

#include <libmount/libmount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#include "fs.h"
#include "mount.h"

#define MOUNT_ERR_BUF_SIZE 1024

#ifdef __clang__
#define ZERO_INIT {}
#else
#define ZERO_INIT {0}
#endif

typedef struct MountArgs {
    const gchar *mountpoint;
    const gchar *device;
    const gchar *fstype;
    const gchar *options;
    const gchar *spec;
    gboolean lazy;
    gboolean force;
} MountArgs;

typedef gboolean (*MountFunc) (MountArgs *args, GError **error);

static gboolean do_mount (MountArgs *args, GError **error);

#ifndef LIBMOUNT_NEW_ERR_API
static gboolean get_unmount_error_old (struct libmnt_context *cxt, int rc, const gchar *spec, GError **error) {
    int syscall_errno = 0;
    int helper_status = 0;

    if (mnt_context_syscall_called (cxt) == 1) {
        syscall_errno = mnt_context_get_syscall_errno (cxt);
        switch (syscall_errno) {
            case 0:
                return TRUE;
            case EBUSY:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Target busy.");
                break;
            case EINVAL:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Not a mount point.");
                break;
            case EPERM:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_AUTH,
                             "Operation not permitted.");
                break;
            default:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Unmount syscall failed: %d.", syscall_errno);
                break;
        }
    } else if (mnt_context_helper_executed (cxt) == 1) {
        helper_status = mnt_context_get_helper_status (cxt);
        if (helper_status != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Unmount helper program failed: %d.", helper_status);
            return FALSE;
        } else
            return TRUE;
    } else {
        if (rc == 0)
            return TRUE;
        else if (rc == -EPERM) {
            if (mnt_context_tab_applied (cxt))
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_AUTH,
                             "Operation not permitted.");
            else
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Not mounted.");
        } else {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Failed to unmount %s.", spec);
        }
    }
    return FALSE;
}
#else
static gboolean get_unmount_error_new (struct libmnt_context *cxt, int rc, const gchar *spec, GError **error) {
    int ret = 0;
    int syscall_errno = 0;
    char buf[MOUNT_ERR_BUF_SIZE] = {0};
    gboolean permission = FALSE;

    ret = mnt_context_get_excode (cxt, rc, buf, MOUNT_ERR_BUF_SIZE - 1);
    if (ret != 0) {
        /* check whether the call failed because of lack of permission */
        if (mnt_context_syscall_called (cxt)) {
            syscall_errno = mnt_context_get_syscall_errno (cxt);
            permission = syscall_errno == EPERM;
        } else
            permission = ret == MNT_EX_USAGE && mnt_context_tab_applied (cxt);

        if (permission)
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_AUTH,
                         "Operation not permitted.");
        else {
            if (*buf == '\0')
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Unknown error when unmounting %s", spec);
            else
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "%s", buf);
        }

        return FALSE;
    }

    return TRUE;
}
#endif

static gboolean do_unmount (MountArgs *args, GError **error) {
    struct libmnt_context *cxt = NULL;
    int ret = 0;
    gboolean success = FALSE;

    cxt = mnt_new_context ();

    if (mnt_context_set_target (cxt, args->spec) != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Failed to set '%s' as target for umount", args->spec);
        mnt_free_context (cxt);
        return FALSE;
    }

    if (args->lazy) {
        if (mnt_context_enable_lazy (cxt, TRUE) != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Failed to set lazy unmount for '%s'", args->spec);
            mnt_free_context (cxt);
            return FALSE;
        }
    }

    if (args->force) {
        if (mnt_context_enable_force (cxt, TRUE) != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Failed to set force unmount for '%s'", args->spec);
            mnt_free_context (cxt);
            return FALSE;
        }
    }

    ret = mnt_context_umount (cxt);
#ifdef LIBMOUNT_NEW_ERR_API
    success = get_unmount_error_new (cxt, ret, args->spec, error);
#else
    success = get_unmount_error_old (cxt, ret, args->spec, error);
#endif

    mnt_free_context (cxt);
    return success;
}

#ifndef LIBMOUNT_NEW_ERR_API
static gboolean get_mount_error_old (struct libmnt_context *cxt, int rc, MountArgs *args, GError **error) {
    int syscall_errno = 0;
    int helper_status = 0;
    unsigned long mflags = 0;

    if (mnt_context_get_mflags (cxt, &mflags) != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Failed to get options from string '%s'.", args->options);
        return FALSE;
    }

    if (mnt_context_syscall_called (cxt) == 1) {
        syscall_errno = mnt_context_get_syscall_errno (cxt);
        switch (syscall_errno) {
            case 0:
                return TRUE;
            case EBUSY:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Source is already mounted or target is busy.");
                break;
            case EINVAL:
                if (mflags & MS_REMOUNT)
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Remount attempted, but %s is not mounted at %s.", args->device, args->mountpoint);
                else if (mflags & MS_MOVE)
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Move attempted, but %s is not a mount point.", args->device);
                else
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Wrong fs type, %s has an invalid superblock or missing helper program.", args->device);
                break;
            case EPERM:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_AUTH,
                             "Operation not permitted.");
                break;
            case ENOTBLK:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "%s is not a block device.", args->device);
                break;
            case ENOTDIR:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "%s is not a directory.", args->mountpoint);
                break;
            case ENODEV:
                if (args->fstype == NULL || strlen (args->fstype) == 0)
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Filesystem type not specified");
                else
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_UNKNOWN_FS,
                                 "Filesystem type %s not configured in kernel.", args->fstype);
                break;
            case EROFS:
            case EACCES:
                  if (mflags & MS_RDONLY) {
                      g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                   "Cannot mount %s read-only.", args->device);
                      break;
                  } else if (args->options && (mnt_optstr_get_option (args->options, "rw", NULL, NULL) == 0)) {
                      g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                   "%s is write-protected but `rw' option given.", args->device);
                      break;
                  } else if (mflags & MS_BIND) {
                      g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                   "Mount %s on %s failed.", args->device, args->mountpoint);
                      break;
                  }
                  /* new versions of libmount do this automatically */
                  else {
                      MountArgs ro_args = ZERO_INIT;
                      gboolean success = FALSE;

                      ro_args.device = args->device;
                      ro_args.mountpoint = args->mountpoint;
                      ro_args.fstype = args->fstype;
                      if (!args->options)
                          ro_args.options = g_strdup ("ro");
                      else
                          ro_args.options = g_strdup_printf ("%s,ro", args->options);

                      success = do_mount (&ro_args, error);

                      g_free ((gchar*) ro_args.options);

                      return success;
                  }
            default:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Mount syscall failed: %d.", syscall_errno);
                break;
        }
    } else if (mnt_context_helper_executed (cxt) == 1) {
        helper_status = mnt_context_get_helper_status (cxt);
        if (helper_status != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Mount helper program failed: %d.", helper_status);
            return FALSE;
        } else
            return TRUE;
    } else {
        switch (rc) {
            case 0:
                return TRUE;
            case -EPERM:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_AUTH,
                             "Only root can mount %s.", args->device);
                break;
            case -EBUSY:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "%s is already mounted.", args->device);
                break;
            /* source or target explicitly defined and not found in fstab */
            case -MNT_ERR_NOFSTAB:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Can't find %s in %s.", args->device ? args->device : args->mountpoint, mnt_get_fstab_path ());
                break;
            case -MNT_ERR_MOUNTOPT:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Failed to parse mount options");
                break;
            case -MNT_ERR_NOSOURCE:
                if (args->device)
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Can't find %s.", args->device);
                else
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Mount source not defined.");
                break;
            case -MNT_ERR_LOOPDEV:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Failed to setup loop device");
                break;
            case -MNT_ERR_NOFSTYPE:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Filesystem type not specified");
                break;
            default:
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Failed to mount %s.", args->device ? args->device : args->mountpoint);
                break;
        }
    }

    return FALSE;
}

#else
static gboolean get_mount_error_new (struct libmnt_context *cxt, int rc, MountArgs *args, GError **error) {
    int ret = 0;
    int syscall_errno = 0;
    char buf[MOUNT_ERR_BUF_SIZE] = {0};
    gboolean permission = FALSE;

    ret = mnt_context_get_excode (cxt, rc, buf, MOUNT_ERR_BUF_SIZE - 1);
    if (ret != 0) {
        /* check whether the call failed because of lack of permission */
        if (mnt_context_syscall_called (cxt) == 1) {
            syscall_errno = mnt_context_get_syscall_errno (cxt);
            permission = syscall_errno == EPERM;
        } else
            permission = ret == MNT_EX_USAGE && mnt_context_tab_applied (cxt);

        if (permission)
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_AUTH,
                         "Operation not permitted.");
        else if (syscall_errno == ENODEV)
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_UNKNOWN_FS,
                         "Filesystem type %s not configured in kernel.", args->fstype);
        else {
            if (*buf == '\0')
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Unknown error when mounting %s", args->device ? args->device : args->mountpoint);
            else
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "%s", buf);
        }

        return FALSE;
    }

    return TRUE;
}
#endif

static gboolean do_mount (MountArgs *args, GError **error) {
    struct libmnt_context *cxt = NULL;
    int ret = 0;
    gboolean success = FALSE;

    cxt = mnt_new_context ();

    if (!args->mountpoint && !args->device) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "You must specify at least one of: mount point, device.");
        mnt_free_context (cxt);
        return FALSE;
    }

    if (args->mountpoint) {
        if (mnt_context_set_target (cxt, args->mountpoint) != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Failed to set '%s' as target for mount", args->mountpoint);
            mnt_free_context (cxt);
            return FALSE;
        }
    }

    if (args->device) {
        if (mnt_context_set_source (cxt, args->device) != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Failed to set '%s' as source for mount", args->device);
            mnt_free_context (cxt);
            return FALSE;
        }
    }

    if (args->fstype) {
        if (mnt_context_set_fstype (cxt, args->fstype) != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Failed to set '%s' as fstype for mount", args->fstype);
            mnt_free_context (cxt);
            return FALSE;
        }
    }

    if (args->options) {
        if (mnt_context_set_options (cxt, args->options) != 0) {
            g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                         "Failed to set '%s' as options for mount", args->options);
            mnt_free_context (cxt);
            return FALSE;
        }
    }

#ifdef LIBMOUNT_NEW_ERR_API
    /* we don't want libmount to try RDONLY mounts if we were explicitly given the "rw" option */
    if (args->options && (mnt_optstr_get_option (args->options, "rw", NULL, NULL) == 0))
        mnt_context_enable_rwonly_mount (cxt, TRUE);
#endif

    ret = mnt_context_mount (cxt);

    /* we need to always do some libmount magic to check if the mount really
       succeeded -- `mnt_context_mount` can return zero when helper program
       (mount.type) fails
     */
#ifdef LIBMOUNT_NEW_ERR_API
    success = get_mount_error_new (cxt, ret, args, error);
#else
    success = get_mount_error_old (cxt, ret, args, error);
#endif

    mnt_free_context (cxt);
    return success;
}

static gboolean set_ruid (uid_t uid, GError **error) {
    if (setresuid (uid, -1, -1) != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Error setting ruid: %m");
        return FALSE;
    }

    return TRUE;
}

static gboolean set_rgid (gid_t gid, GError **error) {
    if (setresgid (gid, -1, -1) != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Error setting rgid: %m");
        return FALSE;
    }

    return TRUE;
}


/**
 * run_as_user:
 *
 * Runs given @func in a child process with real user and group ID specified by
 * @run_as_uid and @run_as_gid. The child process is ended after @func is finished.
 * This is used to run mount and unmount functions in a similar way how the `mount`
 * command, which is a suid binary, works and is used to set the libmount context
 * to restricted.
 */
static gboolean run_as_user (MountFunc func, MountArgs *args, uid_t run_as_uid, gid_t run_as_gid, GError ** error) {
    uid_t current_uid = -1;
    gid_t current_gid = -1;
    pid_t pid = -1;
    pid_t wpid = -1;
    int pipefd[2];
    int status = 0;
    GIOChannel *channel = NULL;
    GError *local_error = NULL;
    gchar *error_msg = NULL;
    gsize msglen = 0;

    current_uid = getuid ();
    current_gid = getgid ();

    if (geteuid () != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Not running as root, cannot change the UID/GID.");
        return FALSE;
    }

    if (pipe(pipefd) == -1) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Error creating pipe.");
        return FALSE;
    }

    pid = fork ();

    if (pid == -1) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Error forking.");
        return FALSE;
    } else if (pid == 0) {
        close (pipefd[0]);

        if (run_as_gid != current_gid) {
            if (!set_rgid (run_as_gid, error)) {
                if (write(pipefd[1], (*error)->message, strlen((*error)->message)) < 0)
                    _exit (BD_FS_ERROR_PIPE);
                else
                    _exit ((*error)->code);
            }
        }

        if (run_as_uid != current_uid) {
            if (!set_ruid (run_as_uid, error)) {
                if (write(pipefd[1], (*error)->message, strlen((*error)->message)) < 0)
                    _exit (BD_FS_ERROR_PIPE);
                else
                    _exit ((*error)->code);
            }
        }

        if (!func (args, error)) {
            if (write(pipefd[1], (*error)->message, strlen((*error)->message)) < 0)
                _exit (BD_FS_ERROR_PIPE);
            else
                _exit ((*error)->code);
        }

        _exit (EXIT_SUCCESS);

    } else {
        close (pipefd[1]);

        do {
            wpid = waitpid (pid, &status, WUNTRACED | WCONTINUED);
            if (wpid == -1) {
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Error while waiting for process.");
                close (pipefd[0]);
                return FALSE;
            }

            if (WIFEXITED (status)) {
              if (WEXITSTATUS (status) != EXIT_SUCCESS) {
                  if (WEXITSTATUS (status) == BD_FS_ERROR_PIPE) {
                      g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                   "Error while reading error.");
                      close (pipefd[0]);
                      return FALSE;
                  }

                  channel = g_io_channel_unix_new (pipefd[0]);
                  if (g_io_channel_read_to_end (channel, &error_msg, &msglen, &local_error) != G_IO_STATUS_NORMAL) {
                      if (local_error) {
                          g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                       "Error while reading error: %s (%d)",
                                       local_error->message, local_error->code);
                          g_clear_error (&local_error);
                      } else
                          g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                       "Unknown error while reading error.");
                      g_io_channel_unref (channel);
                      close (pipefd[0]);
                      g_free (error_msg);
                      return FALSE;
                  }

                  if (g_io_channel_shutdown (channel, TRUE, &local_error) == G_IO_STATUS_ERROR) {
                      g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                   "Error shutting down GIO channel: %s (%d)",
                                   local_error->message, local_error->code);
                      g_clear_error (&local_error);
                      g_io_channel_unref (channel);
                      close (pipefd[0]);
                      g_free (error_msg);
                      return FALSE;
                  }

                  if (WEXITSTATUS (status) > BD_FS_ERROR_AUTH)
                      g_set_error_literal (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                           error_msg);
                  else
                      g_set_error_literal (error, BD_FS_ERROR, WEXITSTATUS (status),
                                           error_msg);

                  g_io_channel_unref (channel);
                  close (pipefd[0]);
                  g_free (error_msg);
                  return FALSE;
              } else {
                  close (pipefd[0]);
                  return TRUE;
              }
            } else if (WIFSIGNALED (status)) {
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Killed by signal %d.", WTERMSIG(status));
                close (pipefd[0]);
                return FALSE;
            }

        } while (!WIFEXITED (status) && !WIFSIGNALED (status));
        close (pipefd[0]);
    }

    return FALSE;
}

/**
 * bd_fs_unmount:
 * @spec: mount point or device to unmount
 * @lazy: enable/disable lazy unmount
 * @force: enable/disable force unmount
 * @extra: (nullable) (array zero-terminated=1): extra options for the unmount;
 *                                               currently only 'run_as_uid'
 *                                               and 'run_as_gid' are supported;
 *                                               value must be a valid non zero
 *                                               uid (gid), if you specify one of
 *                                               these, the function will run in
 *                                               a child process with real user
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: whether @spec was successfully unmounted or not
 *
 * Tech category: %BD_FS_TECH_GENERIC (no mode, ignored)
 */
gboolean bd_fs_unmount (const gchar *spec, gboolean lazy, gboolean force, const BDExtraArg **extra, GError **error) {
    uid_t run_as_uid = -1;
    gid_t run_as_gid = -1;
    uid_t current_uid = -1;
    gid_t current_gid = -1;
    const BDExtraArg **extra_p = NULL;
    gchar *endptr = NULL;
    MountArgs args = ZERO_INIT;
    GError *l_error = NULL;
    gboolean ret = FALSE;

    args.spec = spec;
    args.lazy = lazy;
    args.force = force;

    current_uid = getuid ();
    run_as_uid = current_uid;

    current_gid = getgid ();
    run_as_gid = current_gid;

    if (extra) {
        for (extra_p=extra; *extra_p; extra_p++) {
            if ((*extra_p)->opt && (g_strcmp0 ((*extra_p)->opt, "run_as_uid") == 0)) {
                run_as_uid = g_ascii_strtoull ((*extra_p)->val, &endptr, 0);

                /* g_ascii_strtoull returns 0 in case of error */
                if (run_as_uid == 0 && endptr == (*extra_p)->val) {
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Invalid specification of UID: '%s'", (*extra_p)->val);
                    return FALSE;
                }
            } else if ((*extra_p)->opt && (g_strcmp0 ((*extra_p)->opt, "run_as_gid") == 0)) {
                run_as_gid = g_ascii_strtoull ((*extra_p)->val, &endptr, 0);

                /* g_ascii_strtoull returns 0 in case of error */
                if (run_as_gid == 0 && endptr == (*extra_p)->val) {
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Invalid specification of GID: '%s'", (*extra_p)->val);
                    return FALSE;
                }
            } else {
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Unsupported argument for unmount: '%s'", (*extra_p)->opt);
                return FALSE;
            }
        }
    }

    if (run_as_uid != current_uid || run_as_gid != current_gid) {
        ret = run_as_user ((MountFunc) do_unmount, &args, run_as_uid, run_as_gid, &l_error);
        if (!ret)
            g_propagate_error (error, l_error);
        return ret;
    } else
        return do_unmount (&args, error);

    return TRUE;
}

/**
 * bd_fs_mount:
 * @device: (nullable): device to mount, if not specified @mountpoint entry
 *                        from fstab will be used
 * @mountpoint: (nullable): mountpoint for @device, if not specified @device
 *                            entry from fstab will be used
 * @fstype: (nullable): filesystem type
 * @options: (nullable): comma delimited options for mount
 * @extra: (nullable) (array zero-terminated=1): extra options for the mount;
 *                                               currently only 'run_as_uid'
 *                                               and 'run_as_gid' are supported;
 *                                               value must be a valid non zero
 *                                               uid (gid), if you specify one of
 *                                               these, the function will run in
 *                                               a child process with real user
 *                                               and/or group ID set to these values.
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: whether @device (or @mountpoint) was successfully mounted or not
 *
 * Tech category: %BD_FS_TECH_MOUNT (no mode, ignored)
 */
gboolean bd_fs_mount (const gchar *device, const gchar *mountpoint, const gchar *fstype, const gchar *options, const BDExtraArg **extra, GError **error) {
    uid_t run_as_uid = -1;
    gid_t run_as_gid = -1;
    uid_t current_uid = -1;
    gid_t current_gid = -1;
    const BDExtraArg **extra_p = NULL;
    gchar *endptr = NULL;
    MountArgs args = ZERO_INIT;
    GError *l_error = NULL;
    gboolean ret = FALSE;

    args.device = device;
    args.mountpoint = mountpoint;
    args.fstype = fstype;
    args.options = options;

    current_uid = getuid ();
    run_as_uid = current_uid;

    current_gid = getgid ();
    run_as_gid = current_gid;

    if (extra) {
        for (extra_p=extra; *extra_p; extra_p++) {
            if ((*extra_p)->opt && (g_strcmp0 ((*extra_p)->opt, "run_as_uid") == 0)) {
                run_as_uid = g_ascii_strtoull ((*extra_p)->val, &endptr, 0);

                /* g_ascii_strtoull returns 0 in case of error */
                if (run_as_uid == 0 && endptr == (*extra_p)->val) {
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Invalid specification of UID: '%s'", (*extra_p)->val);
                    return FALSE;
                }
            } else if ((*extra_p)->opt && (g_strcmp0 ((*extra_p)->opt, "run_as_gid") == 0)) {
                run_as_gid = g_ascii_strtoull ((*extra_p)->val, &endptr, 0);

                /* g_ascii_strtoull returns 0 in case of error */
                if (run_as_gid == 0 && endptr == (*extra_p)->val) {
                    g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                                 "Invalid specification of GID: '%s'", (*extra_p)->val);
                    return FALSE;
                }
            } else {
                g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                             "Unsupported argument for unmount: '%s'", (*extra_p)->opt);
                return FALSE;
            }
        }
    }

    if (run_as_uid != current_uid || run_as_gid != current_gid) {
        ret = run_as_user ((MountFunc) do_mount, &args, run_as_uid, run_as_gid, &l_error);
        if (!ret)
            g_propagate_error (error, l_error);
        return ret;
    } else
       return do_mount (&args, error);

    return TRUE;
}

/**
 * bd_fs_get_mountpoint:
 * @device: device to find mountpoint for
 * @error: (out) (optional): place to store error (if any)
 *
 * Get mountpoint for @device. If @device is mounted multiple times only
 * one mountpoint will be returned.
 *
 * Returns: (transfer full): mountpoint for @device, %NULL in case device is
 *                           not mounted or in case of an error (@error is set
 *                           in this case)
 *
 * Tech category: %BD_FS_TECH_MOUNT (no mode, ignored)
 */
gchar* bd_fs_get_mountpoint (const gchar *device, GError **error) {
    struct libmnt_table *table = NULL;
    struct libmnt_fs *fs = NULL;
    struct libmnt_cache *cache = NULL;
    gint ret = 0;
    gchar *mountpoint = NULL;
    const gchar *target = NULL;

    table = mnt_new_table ();
    cache = mnt_new_cache ();

    ret = mnt_table_set_cache (table, cache);
    if (ret != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Failed to set cache for mount info table.");
        mnt_free_table (table);
        return NULL;
    }

    ret = mnt_table_parse_mtab (table, NULL);
    if (ret != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Failed to parse mount info.");
        mnt_free_table (table);
        mnt_free_cache (cache);
        return NULL;
    }

    fs = mnt_table_find_source (table, device, MNT_ITER_FORWARD);
    if (!fs) {
        mnt_free_table (table);
        mnt_free_cache (cache);
        return NULL;
    }

    target = mnt_fs_get_target (fs);
    if (!target) {
        mnt_free_fs (fs);
        mnt_free_table (table);
        mnt_free_cache (cache);
        return NULL;
    }

    mountpoint = g_strdup (target);
    mnt_free_fs (fs);
    mnt_free_table (table);
    mnt_free_cache (cache);
    return mountpoint;
}

/**
 * bd_fs_is_mountpoint:
 * @path: path (folder) to check
 * @error: (out) (optional): place to store error (if any)
 *
 * Returns: whether @path is a mountpoint or not
 *
 * Tech category: %BD_FS_TECH_MOUNT (no mode, ignored)
 */
gboolean bd_fs_is_mountpoint (const gchar *path, GError **error) {
    struct libmnt_table *table = NULL;
    struct libmnt_fs *fs = NULL;
    struct libmnt_cache *cache = NULL;
    const gchar *target = NULL;
    gint ret = 0;

    table = mnt_new_table ();
    cache = mnt_new_cache ();

    ret = mnt_table_set_cache (table, cache);
    if (ret != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Failed to set cache for mount info table.");
        mnt_free_table (table);
        return FALSE;
    }

    ret = mnt_table_parse_mtab (table, NULL);
    if (ret != 0) {
        g_set_error (error, BD_FS_ERROR, BD_FS_ERROR_FAIL,
                     "Failed to parse mount info.");
        mnt_free_table (table);
        mnt_free_cache (cache);
        return FALSE;
    }

    fs = mnt_table_find_target (table, path, MNT_ITER_BACKWARD);
    if (!fs) {
        mnt_free_table (table);
        mnt_free_cache (cache);
        return FALSE;
    }

    target = mnt_fs_get_target (fs);
    if (!target) {
        mnt_free_fs (fs);
        mnt_free_table (table);
        mnt_free_cache (cache);
        return FALSE;
    }

    mnt_free_fs (fs);
    mnt_free_table (table);
    mnt_free_cache (cache);
    return TRUE;
}
