/*
 * Copyright (C) 2017 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "locking.h"
#include "locking.c"

#include "../libsnap-confine-private/cleanup-funcs.h"
#include "../libsnap-confine-private/test-utils.h"

#include <errno.h>

#include <glib.h>
#include <glib/gstdio.h>

// Set alternate locking directory
static void sc_set_lock_dir(const char *dir) { sc_lock_dir = dir; }

// A variant of unsetenv that is compatible with GDestroyNotify
static void my_unsetenv(const char *k) { unsetenv(k); }

// Use temporary directory for locking.
//
// The directory is automatically reset to the real value at the end of the
// test.
static const char *sc_test_use_fake_lock_dir(void) {
    char *lock_dir = NULL;
    if (g_test_subprocess()) {
        // Check if the environment variable is set. If so then someone is already
        // managing the temporary directory and we should not create a new one.
        lock_dir = getenv("SNAP_CONFINE_LOCK_DIR");
        g_assert_nonnull(lock_dir);
    } else {
        lock_dir = g_dir_make_tmp(NULL, NULL);
        g_assert_nonnull(lock_dir);
        g_test_queue_free(lock_dir);
        g_assert_cmpint(setenv("SNAP_CONFINE_LOCK_DIR", lock_dir, 0), ==, 0);
        g_test_queue_destroy((GDestroyNotify)my_unsetenv, "SNAP_CONFINE_LOCK_DIR");
        g_test_queue_destroy((GDestroyNotify)rm_rf_tmp, lock_dir);
    }
    g_test_queue_destroy((GDestroyNotify)sc_set_lock_dir, SC_LOCK_DIR);
    sc_set_lock_dir(lock_dir);
    return lock_dir;
}

// Check that locking a namespace actually flock's the mutex with LOCK_EX
static void test_sc_lock_unlock(void) {
    if (geteuid() != 0) {
        g_test_skip("this test only runs as root");
        return;
    }

    const char *lock_dir = sc_test_use_fake_lock_dir();
    int fd = sc_lock_generic("foo", 123);
    // Construct the name of the lock file
    char *lock_file SC_CLEANUP(sc_cleanup_string) = NULL;
    lock_file = g_strdup_printf("%s/foo.123.lock", lock_dir);
    // Open the lock file again to obtain a separate file descriptor.
    // According to flock(2) locks are associated with an open file table entry
    // so this descriptor will be separate and can compete for the same lock.
    int lock_fd SC_CLEANUP(sc_cleanup_close) = -1;
    lock_fd = open(lock_file, O_RDWR | O_CLOEXEC | O_NOFOLLOW);
    g_assert_cmpint(lock_fd, !=, -1);
    // The non-blocking lock operation should fail with EWOULDBLOCK as the lock
    // file is locked by sc_nlock_ns_mutex() already.
    int err = flock(lock_fd, LOCK_EX | LOCK_NB);
    int saved_errno = errno;
    g_assert_cmpint(err, ==, -1);
    g_assert_cmpint(saved_errno, ==, EWOULDBLOCK);
    // Unlock the lock.
    sc_unlock(fd);
    // Re-attempt the locking operation. This time it should succeed.
    err = flock(lock_fd, LOCK_EX | LOCK_NB);
    g_assert_cmpint(err, ==, 0);
}

// Check that holding a lock is properly detected.
static void test_sc_verify_snap_lock__locked(void) {
    if (geteuid() != 0) {
        g_test_skip("this test only runs as root");
        return;
    }

    (void)sc_test_use_fake_lock_dir();
    int fd = sc_lock_snap("foo");
    sc_verify_snap_lock("foo");
    sc_unlock(fd);
}

// Check that holding a lock is properly detected.
static void test_sc_verify_snap_lock__unlocked(void) {
    if (geteuid() != 0) {
        g_test_skip("this test only runs as root");
        return;
    }

    (void)sc_test_use_fake_lock_dir();
    if (g_test_subprocess()) {
        sc_verify_snap_lock("foo");
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("unexpectedly managed to acquire exclusive lock over snap foo\n");
}

static void test_sc_enable_sanity_timeout(void) {
    if (geteuid() != 0) {
        g_test_skip("this test only runs as root");
        return;
    }

    if (g_test_subprocess()) {
        sc_enable_sanity_timeout();
        debug("waiting...");
        usleep(7 * G_USEC_PER_SEC);
        debug("woke up");
        sc_disable_sanity_timeout();
        return;
    }
    g_test_trap_subprocess(NULL, 1 * G_USEC_PER_SEC, G_TEST_SUBPROCESS_INHERIT_STDERR);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("sanity timeout expired: Interrupted system call\n");
}

// Set alternate inhibit directory
static void sc_set_inhibit_dir(const char *dir) { sc_inhibit_dir = dir; }

// Use temporary directory for inhibition.
//
// The directory is automatically reset to the real value at the end of the
// test.
static const char *sc_test_use_fake_inhibit_dir(void) {
    char *inhibit_dir = g_dir_make_tmp(NULL, NULL);
    g_assert_nonnull(inhibit_dir);
    g_test_queue_free(inhibit_dir);
    g_test_queue_destroy((GDestroyNotify)rm_rf_tmp, inhibit_dir);
    g_test_queue_destroy((GDestroyNotify)sc_set_inhibit_dir, SC_INHIBIT_DIR);
    sc_set_inhibit_dir(inhibit_dir);
    return inhibit_dir;
}

static void test_sc_snap_is_inhibited__missing_dir(void) {
    char *d = g_dir_make_tmp(NULL, NULL);
    g_assert_nonnull(d);
    g_test_queue_free(d);
    g_test_queue_destroy((GDestroyNotify)rm_rf_tmp, d);

    char subd[PATH_MAX];
    sc_must_snprintf(subd, sizeof subd, "%s/absent", d);
    sc_set_inhibit_dir(subd);
    g_assert_false(sc_snap_is_inhibited("foo", SC_SNAP_HINT_INHIBITED_FOR_REMOVE));
}

static void test_sc_snap_is_inhibited__missing_file(void) {
    sc_test_use_fake_inhibit_dir();
    g_assert_false(sc_snap_is_inhibited("foo", SC_SNAP_HINT_INHIBITED_FOR_REMOVE));
}

static void test_sc_snap_is_inhibited__present_file(void) {
    const char *d = sc_test_use_fake_inhibit_dir();
    char pname[PATH_MAX];
    sc_must_snprintf(pname, sizeof pname, "%s/foo.remove", d);
    g_assert_true(g_file_set_contents(pname, "", -1, NULL));

    g_assert_true(sc_snap_is_inhibited("foo", SC_SNAP_HINT_INHIBITED_FOR_REMOVE));
}

static void __attribute__((constructor)) init(void) {
    g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock);
    g_test_add_func("/locking/sc_enable_sanity_timeout", test_sc_enable_sanity_timeout);
    g_test_add_func("/locking/sc_verify_snap_lock__locked", test_sc_verify_snap_lock__locked);
    g_test_add_func("/locking/sc_verify_snap_lock__unlocked", test_sc_verify_snap_lock__unlocked);
    g_test_add_func("/locking/sc_snap_is_inhibited__missing_dir", test_sc_snap_is_inhibited__missing_dir);
    g_test_add_func("/locking/sc_snap_is_inhibited__missing_file", test_sc_snap_is_inhibited__missing_file);
    g_test_add_func("/locking/sc_snap_is_inhibited__present_file", test_sc_snap_is_inhibited__present_file);
}
