#ifndef __FreeBSD__
#define _DEFAULT_SOURCE
#endif

#include <sys/types.h>

#include <dirent.h>
#include <err.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct observed_entry {
	char *name;
	off_t cookie;
	unsigned char type;
};

struct observed_list {
	struct observed_entry *entries;
	size_t count;
	size_t capacity;
};

static void
append_entry(struct observed_list *list, const char *name, off_t cookie,
    unsigned char type)
{
	struct observed_entry *entries;

	if (list->count == list->capacity) {
		list->capacity = list->capacity == 0 ? 64 : list->capacity * 2;
		entries = realloc(list->entries,
		    list->capacity * sizeof(*list->entries));
		if (entries == NULL)
			err(1, "realloc entries");
		list->entries = entries;
	}
	list->entries[list->count].name = strdup(name);
	if (list->entries[list->count].name == NULL)
		err(1, "strdup");
	list->entries[list->count].cookie = cookie;
	list->entries[list->count].type = type;
	list->count++;
}

static uint64_t
entry_hash(const struct observed_list *list)
{
	uint64_t hash;

	hash = UINT64_C(14695981039346656037);
	for (size_t index = 0; index < list->count; index++) {
		for (const unsigned char *name =
		    (const unsigned char *)list->entries[index].name;
		    *name != '\0'; name++) {
			hash ^= *name;
			hash *= UINT64_C(1099511628211);
		}
		hash ^= 0;
		hash *= UINT64_C(1099511628211);
		hash ^= list->entries[index].type;
		hash *= UINT64_C(1099511628211);
	}
	return (hash);
}

static void
print_type_counts(const struct observed_list *list)
{
	size_t block, character, directory, fifo, link, other, regular, socket,
	    unknown;

	block = character = directory = fifo = link = other = regular = socket =
	    unknown = 0;
	for (size_t index = 0; index < list->count; index++) {
		switch (list->entries[index].type) {
		case DT_BLK:
			block++;
			break;
		case DT_CHR:
			character++;
			break;
		case DT_DIR:
			directory++;
			break;
		case DT_FIFO:
			fifo++;
			break;
		case DT_LNK:
			link++;
			break;
		case DT_REG:
			regular++;
			break;
		case DT_SOCK:
			socket++;
			break;
		case DT_UNKNOWN:
			unknown++;
			break;
		default:
			other++;
			break;
		}
	}
	printf("types=dir:%zu,reg:%zu,lnk:%zu,chr:%zu,blk:%zu,fifo:%zu,"
	    "sock:%zu,unknown:%zu,other:%zu ", directory, regular, link,
	    character, block, fifo, socket, unknown, other);
}

static void
free_list(struct observed_list *list)
{
	for (size_t index = 0; index < list->count; index++)
		free(list->entries[index].name);
	free(list->entries);
}

static void
check_unique_names(const struct observed_list *list)
{
	for (size_t left = 0; left < list->count; left++) {
		for (size_t right = left + 1; right < list->count; right++) {
			if (strcmp(list->entries[left].name,
			    list->entries[right].name) == 0)
				errx(1, "duplicate name: %s",
				    list->entries[left].name);
		}
	}
}

static struct observed_list
scan_getdirentries(const char *path, size_t buffer_size)
{
	struct observed_list list = { 0 };
	struct dirent *entry;
	char *buffer, *end;
	off_t base, previous;
	ssize_t bytes;
	int fd;

	fd = open(path, O_RDONLY | O_DIRECTORY);
	if (fd < 0)
		err(1, "open %s", path);
	buffer = malloc(buffer_size);
	if (buffer == NULL)
		err(1, "malloc");
	previous = 0;
	for (;;) {
		base = -1;
		bytes = getdirentries(fd, buffer, buffer_size, &base);
		if (bytes < 0)
			err(1, "getdirentries %s", path);
		if (bytes == 0)
			break;
		end = buffer + bytes;
		for (entry = (struct dirent *)buffer; (char *)entry < end;
		    entry = (struct dirent *)((char *)entry + entry->d_reclen)) {
			if (entry->d_reclen == 0 ||
			    (char *)entry + entry->d_reclen > end)
				errx(1, "invalid dirent record");
			if (entry->d_off <= previous)
				errx(1, "non-increasing d_off %jd after %jd",
				    (intmax_t)entry->d_off, (intmax_t)previous);
			append_entry(&list, entry->d_name, entry->d_off,
			    entry->d_type);
			previous = entry->d_off;
		}
	}
	free(buffer);
	if (close(fd) != 0)
		err(1, "close %s", path);
	check_unique_names(&list);
	return (list);
}

static void
verify_kernel_cookies(const char *path, size_t buffer_size,
    const struct observed_list *list)
{
	struct dirent *entry;
	char *buffer;
	off_t base;
	ssize_t bytes;
	int fd;

	buffer = malloc(buffer_size);
	if (buffer == NULL)
		err(1, "malloc restart buffer");
	for (size_t index = 0; index < list->count; index++) {
		fd = open(path, O_RDONLY | O_DIRECTORY);
		if (fd < 0)
			err(1, "open restart %s", path);
		if (lseek(fd, list->entries[index].cookie, SEEK_SET) < 0)
			err(1, "lseek cookie %jd",
			    (intmax_t)list->entries[index].cookie);
		base = -1;
		bytes = getdirentries(fd, buffer, buffer_size, &base);
		if (bytes < 0)
			err(1, "getdirentries restart");
		if (index + 1 == list->count) {
			if (bytes != 0)
				errx(1, "final d_off cookie did not reach EOF");
		} else {
			if (bytes <= 0)
				errx(1, "restart cookie returned no dirent");
			entry = (struct dirent *)buffer;
			if (entry->d_reclen == 0 || entry->d_reclen > (size_t)bytes)
				errx(1, "restart cookie returned a truncated dirent");
			if (strcmp(entry->d_name, list->entries[index + 1].name) != 0)
				errx(1, "cookie %jd resumed at %s, expected %s",
				    (intmax_t)list->entries[index].cookie,
				    entry->d_name, list->entries[index + 1].name);
		}
		if (close(fd) != 0)
			err(1, "close restart");
	}
	free(buffer);
}

static struct observed_list
scan_readdir(const char *path)
{
	struct observed_list list = { 0 };
	struct dirent *entry;
	DIR *directory;
	long cookie, previous;

	directory = opendir(path);
	if (directory == NULL)
		err(1, "opendir %s", path);
	previous = 0;
	while ((entry = readdir(directory)) != NULL) {
		cookie = telldir(directory);
		if (cookie <= previous)
			errx(1, "non-increasing telldir cookie %ld after %ld",
			    cookie, previous);
		append_entry(&list, entry->d_name, (off_t)cookie,
		    entry->d_type);
		previous = cookie;
	}
	if (closedir(directory) != 0)
		err(1, "closedir %s", path);
	check_unique_names(&list);
	return (list);
}

static void
verify_seekdir(const char *path, const struct observed_list *list)
{
	struct dirent *entry;
	DIR *directory;
	long cookie;

	for (size_t index = 0; index < list->count; index++) {
		directory = opendir(path);
		if (directory == NULL)
			err(1, "opendir restart %s", path);
		for (size_t position = 0; position <= index; position++) {
			entry = readdir(directory);
			if (entry == NULL ||
			    strcmp(entry->d_name, list->entries[position].name) != 0)
				errx(1, "seekdir setup differs at entry %zu",
				    position);
		}
		cookie = telldir(directory);
		if ((off_t)cookie != list->entries[index].cookie)
			errx(1, "telldir cookie differs at entry %zu", index);
		seekdir(directory, cookie);
		entry = readdir(directory);
		if (index + 1 == list->count) {
			if (entry != NULL)
				errx(1, "final telldir cookie did not reach EOF");
		} else if (entry == NULL ||
		    strcmp(entry->d_name, list->entries[index + 1].name) != 0) {
			errx(1, "seekdir cookie %jd resumed at %s, expected %s",
			    (intmax_t)list->entries[index].cookie,
			    entry == NULL ? "EOF" : entry->d_name,
			    list->entries[index + 1].name);
		}
		if (closedir(directory) != 0)
			err(1, "closedir restart");
	}
}

int
main(int argc, char **argv)
{
	struct observed_list kernel, libc;
	char *end;
	unsigned long raw_size;
	size_t buffer_size;

	if (argc < 2 || argc > 3)
		errx(2, "usage: %s directory [buffer-size]", argv[0]);
	buffer_size = 128;
	if (argc == 3) {
		raw_size = strtoul(argv[2], &end, 0);
		if (*argv[2] == '\0' || *end != '\0' || raw_size < 64 ||
		    raw_size > 1024 * 1024)
			errx(2, "invalid buffer size: %s", argv[2]);
		buffer_size = (size_t)raw_size;
	}
	kernel = scan_getdirentries(argv[1], buffer_size);
	verify_kernel_cookies(argv[1], buffer_size, &kernel);
	libc = scan_readdir(argv[1]);
	verify_seekdir(argv[1], &libc);
	if (kernel.count != libc.count)
		errx(1, "getdirentries count %zu differs from readdir count %zu",
		    kernel.count, libc.count);
	for (size_t index = 0; index < kernel.count; index++) {
		if (strcmp(kernel.entries[index].name, libc.entries[index].name) != 0)
			errx(1, "API order differs at entry %zu", index);
		if (kernel.entries[index].type != libc.entries[index].type)
			errx(1, "API type differs at entry %zu", index);
	}
	print_type_counts(&kernel);
	printf("entries=%zu d_off_restarts=%zu seekdir_restarts=%zu "
	    "buffer=%zu fnv1a64=%016" PRIx64 "\n", kernel.count,
	    kernel.count, libc.count, buffer_size, entry_hash(&kernel));
	free_list(&libc);
	free_list(&kernel);
	return (0);
}
