#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

/* Run only in a fresh private directory supplied by observe.py. */
static void require(int ok, const char *what) {
    if (!ok) { perror(what); exit(1); }
}
static int same(int a, int b) {
    struct stat x, y;
    require(fstat(a, &x) == 0 && fstat(b, &y) == 0, "fstat");
    return x.st_ino == y.st_ino && x.st_dev == y.st_dev;
}
static int contents(int fd, const char *expected) {
    char b[32] = {0};
    ssize_t n = pread(fd, b, sizeof(b), 0);
    require(n >= 0, "pread");
    return n == (ssize_t)strlen(expected) && memcmp(b, expected, (size_t)n) == 0;
}
static void put(int fd, const char *s) {
    size_t n = strlen(s), done = 0;
    while (done < n) {
        ssize_t count = write(fd, s + done, n - done);
        if (count < 0 && errno == EINTR) continue;
        require(count > 0, "write");
        done += (size_t)count;
    }
}
static int publish(int fd, int dir, const char *name) {
    char proc[64];
    snprintf(proc, sizeof(proc), "/proc/self/fd/%d", fd);
    return linkat(AT_FDCWD, proc, dir, name, AT_SYMLINK_FOLLOW);
}
static void row(const char *id, int rc, int error, int invariant, int last) {
    require(invariant, id);
    printf("    {\"id\":\"%s\",\"returnValue\":%d,\"errno\":%d,\"invariant\":true}%s\n",
           id, rc, error, last ? "" : ",");
}
static int missing(int dir, const char *name) {
    struct stat s;
    errno = 0;
    return fstatat(dir, name, &s, AT_SYMLINK_NOFOLLOW) == -1 && errno == ENOENT;
}
int main(int argc, char **argv) {
    require(argc == 2, "private directory argument required");
    int dir = open(argv[1], O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    require(dir >= 0, "open directory");
    int tmp = openat(dir, ".", O_TMPFILE | O_RDWR | O_CLOEXEC, 0600);
    require(tmp >= 0, "O_TMPFILE (failure is not a passing observation)");
    put(tmp, "new");
    struct stat before, after;
    require(fstat(tmp, &before) == 0, "fstat unnamed");
    printf("{\"cases\":[\n");
    row("unnamed", 0, 0, before.st_nlink == 0 && missing(dir, "published"), 0);

    require(fsync(tmp) == 0, "fsync file");
    int rc = publish(tmp, dir, "published");
    int error = rc < 0 ? errno : 0;
    require(rc == 0, "publish");
    int published = openat(dir, "published", O_RDONLY | O_CLOEXEC);
    require(published >= 0 && fstat(tmp, &after) == 0, "open published");
    row("publish-new-name", rc, error,
        after.st_nlink == 1 && same(tmp, published) && contents(published, "new"), 0);
    require(fsync(dir) == 0, "fsync directory");

    int collision = openat(dir, ".", O_TMPFILE | O_RDWR | O_CLOEXEC, 0600);
    require(collision >= 0, "open collision candidate");
    put(collision, "candidate");
    rc = publish(collision, dir, "published");
    error = rc < 0 ? errno : 0;
    int named = openat(dir, "published", O_RDONLY | O_CLOEXEC);
    require(named >= 0 && fstat(collision, &after) == 0, "inspect collision");
    row("link-collision", rc, error,
        rc == -1 && error == EEXIST && after.st_nlink == 0
        && same(named, published) && contents(named, "new"), 0);

    int exclusive = openat(dir, ".", O_TMPFILE | O_EXCL | O_RDWR | O_CLOEXEC, 0600);
    require(exclusive >= 0, "O_TMPFILE O_EXCL");
    rc = publish(exclusive, dir, "exclusive");
    error = rc < 0 ? errno : 0;
    row("exclusive-cannot-publish", rc, error,
        rc == -1 && error == ENOENT && missing(dir, "exclusive"), 0);

    int unlinked = openat(dir, "ordinary", O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0600);
    require(unlinked >= 0, "create ordinary file");
    put(unlinked, "still-open");
    require(unlinkat(dir, "ordinary", 0) == 0, "remove ordinary file's last name");
    require(fstat(unlinked, &before) == 0 && before.st_nlink == 0, "zero links after unlink");
    rc = publish(unlinked, dir, "revived");
    error = rc < 0 ? errno : 0;
    require(fstat(unlinked, &after) == 0, "inspect rejected relink");
    row("unlinked-cannot-publish", rc, error,
        rc == -1 && error == ENOENT && after.st_nlink == 0
        && missing(dir, "ordinary") && missing(dir, "revived")
        && contents(unlinked, "still-open"), 0);

    int left = openat(dir, "left", O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0600);
    int right = openat(dir, "right", O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0600);
    require(left >= 0 && right >= 0, "create rename inputs");
    put(left, "left-data"); put(right, "right-data");
    rc = renameat2(dir, "left", dir, "right", RENAME_NOREPLACE);
    error = rc < 0 ? errno : 0;
    int l = openat(dir, "left", O_RDONLY | O_CLOEXEC);
    int r = openat(dir, "right", O_RDONLY | O_CLOEXEC);
    require(l >= 0 && r >= 0, "open after no-replace");
    row("noreplace-collision", rc, error,
        rc == -1 && error == EEXIST && same(l, left) && same(r, right)
        && contents(l, "left-data") && contents(r, "right-data"), 0);
    require(close(l) == 0 && close(r) == 0, "close inspection descriptors");

    rc = renameat2(dir, "left", dir, "moved", RENAME_NOREPLACE);
    error = rc < 0 ? errno : 0;
    l = openat(dir, "moved", O_RDONLY | O_CLOEXEC);
    require(l >= 0, "open moved");
    row("noreplace-new-name", rc, error,
        rc == 0 && missing(dir, "left") && same(l, left), 0);
    require(close(l) == 0, "close moved inspection");

    rc = renameat2(dir, "moved", dir, "right", RENAME_EXCHANGE);
    error = rc < 0 ? errno : 0;
    l = openat(dir, "moved", O_RDONLY | O_CLOEXEC);
    r = openat(dir, "right", O_RDONLY | O_CLOEXEC);
    require(l >= 0 && r >= 0, "open after exchange");
    row("exchange", rc, error,
        rc == 0 && same(l, right) && same(r, left)
        && contents(left, "left-data") && contents(right, "right-data")
        && contents(l, "right-data") && contents(r, "left-data"), 0);
    require(close(l) == 0 && close(r) == 0, "close exchange inspection");

    rc = renameat2(dir, "moved", dir, "absent", RENAME_EXCHANGE);
    error = rc < 0 ? errno : 0;
    row("exchange-missing", rc, error,
        rc == -1 && error == ENOENT && missing(dir, "absent"), 0);
    rc = renameat2(dir, "moved", dir, "right", RENAME_EXCHANGE | RENAME_NOREPLACE);
    error = rc < 0 ? errno : 0;
    l = openat(dir, "moved", O_RDONLY | O_CLOEXEC);
    r = openat(dir, "right", O_RDONLY | O_CLOEXEC);
    require(l >= 0 && r >= 0, "open after rejected flags");
    row("incompatible-flags", rc, error,
        rc == -1 && error == EINVAL && same(l, right) && same(r, left), 1);
    int fds[] = {l, r, unlinked, left, right, exclusive, named, collision, published, tmp, dir};
    for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++)
        require(close(fds[i]) == 0, "close");
    printf("]}\n");
    return 0;
}
