// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *
 *  BlueZ - Bluetooth protocol stack for Linux
 *
 *  Copyright (C) 2004-2010  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/param.h>

#include "textfile.h"

int create_filename(char *str, size_t size, const char *fmt, ...)
{
	static const char *prefix;
	static int prefix_len;
	char suffix[PATH_MAX];
	va_list ap;
	int err;

	if (!prefix) {
		const char *statedir = getenv("STATE_DIRECTORY");

		/* Check if running as service */
		if (statedir) {
			prefix = statedir;

			/* Check if there multiple paths given */
			if (strstr(prefix, ":"))
				prefix_len = strstr(prefix, ":") - prefix;
			else
				prefix_len = strlen(prefix);
		} else {
			prefix = STORAGEDIR;
			prefix_len = strlen(prefix);
		}
	}

	va_start(ap, fmt);
	err = vsnprintf(suffix, sizeof(suffix), fmt, ap);
	va_end(ap);

	if (err < 0)
		return err;

	return snprintf(str, size, "%*s%s", prefix_len, prefix, suffix);
}

static int create_dirs(const char *filename, const mode_t mode)
{
	struct stat st;
	char dir[PATH_MAX + 1], *prev, *next;
	int err;

	err = stat(filename, &st);
	if (!err && S_ISREG(st.st_mode))
		return 0;

	memset(dir, 0, PATH_MAX + 1);
	strcat(dir, "/");

	prev = strchr(filename, '/');

	while (prev) {
		next = strchr(prev + 1, '/');
		if (!next)
			break;

		if (next - prev == 1) {
			prev = next;
			continue;
		}

		strncat(dir, prev + 1, next - prev);
		mkdir(dir, mode);

		prev = next;
	}

	return 0;
}

int create_file(const char *filename, const mode_t mode)
{
	int fd;

	create_dirs(filename, 0700);

	fd = open(filename, O_RDWR | O_CREAT, mode);
	if (fd < 0)
		return fd;

	close(fd);

	return 0;
}

int create_name(char *buf, size_t size, const char *address, const char *name)
{
	return create_filename(buf, size, "/%s/%s", address, name);
}

static inline char *find_key(char *map, size_t size, const char *key, size_t len, int icase)
{
	char *ptr = map;
	size_t ptrlen = size;

	while (ptrlen > len + 1) {
		int cmp = (icase) ? strncasecmp(ptr, key, len) : strncmp(ptr, key, len);
		if (cmp == 0) {
			if (ptr == map && *(ptr + len) == ' ')
				return ptr;

			if ((*(ptr - 1) == '\r' || *(ptr - 1) == '\n') &&
							*(ptr + len) == ' ')
				return ptr;
		}

		if (icase) {
			char *p1 = memchr(ptr + 1, tolower(*key), ptrlen - 1);
			char *p2 = memchr(ptr + 1, toupper(*key), ptrlen - 1);

			if (!p1)
				ptr = p2;
			else if (!p2)
				ptr = p1;
			else
				ptr = (p1 < p2) ? p1 : p2;
		} else
			ptr = memchr(ptr + 1, *key, ptrlen - 1);

		if (!ptr)
			return NULL;

		ptrlen = size - (ptr - map);
	}

	return NULL;
}

static inline int write_key_value(int fd, const char *key, const char *value)
{
	char *str;
	size_t size;
	int err = 0;

	size = strlen(key) + strlen(value) + 2;

	str = malloc(size + 1);
	if (!str)
		return ENOMEM;

	sprintf(str, "%s %s\n", key, value);

	if (write(fd, str, size) < 0)
		err = -errno;

	free(str);

	return err;
}

static char *strnpbrk(const char *s, ssize_t len, const char *accept)
{
	const char *p = s;
	const char *end;

	end = s + len - 1;

	while (p <= end && *p) {
		const char *a = accept;

		while (*a) {
			if (*p == *a)
				return (char *) p;
			a++;
		}

		p++;
	}

	return NULL;
}

static int write_key(const char *pathname, const char *key, const char *value, int icase)
{
	struct stat st;
	char *map, *off, *end, *str;
	off_t size;
	size_t base;
	int fd, len, err = 0;

	fd = open(pathname, O_RDWR);
	if (fd < 0)
		return -errno;

	if (flock(fd, LOCK_EX) < 0) {
		err = -errno;
		goto close;
	}

	if (fstat(fd, &st) < 0) {
		err = -errno;
		goto unlock;
	}

	size = st.st_size;

	if (!size) {
		if (value) {
			lseek(fd, size, SEEK_SET);
			err = write_key_value(fd, key, value);
		}
		goto unlock;
	}

	map = mmap(NULL, size, PROT_READ | PROT_WRITE,
					MAP_PRIVATE | MAP_LOCKED, fd, 0);
	if (!map || map == MAP_FAILED) {
		err = -errno;
		goto unlock;
	}

	len = strlen(key);
	off = find_key(map, size, key, len, icase);
	if (!off) {
		munmap(map, size);
		if (value) {
			lseek(fd, size, SEEK_SET);
			err = write_key_value(fd, key, value);
		}
		goto unlock;
	}

	base = off - map;

	end = strnpbrk(off, size, "\r\n");
	if (!end) {
		err = -EILSEQ;
		goto unmap;
	}

	if (value && ((ssize_t) strlen(value) == end - off - len - 1) &&
			!strncmp(off + len + 1, value, end - off - len - 1))
		goto unmap;

	len = strspn(end, "\r\n");
	end += len;

	len = size - (end - map);
	if (!len) {
		munmap(map, size);
		if (ftruncate(fd, base) < 0) {
			err = -errno;
			goto unlock;
		}
		lseek(fd, base, SEEK_SET);
		if (value)
			err = write_key_value(fd, key, value);

		goto unlock;
	}

	if (len < 0 || len > size) {
		err = -EILSEQ;
		goto unmap;
	}

	str = malloc(len);
	if (!str) {
		err = -errno;
		goto unmap;
	}

	memcpy(str, end, len);

	munmap(map, size);
	if (ftruncate(fd, base) < 0) {
		err = -errno;
		free(str);
		goto unlock;
	}
	lseek(fd, base, SEEK_SET);
	if (value)
		err = write_key_value(fd, key, value);

	if (write(fd, str, len) < 0)
		err = -errno;

	free(str);

	goto unlock;

unmap:
	munmap(map, size);

unlock:
	flock(fd, LOCK_UN);

close:
	fdatasync(fd);

	close(fd);
	errno = -err;

	return err;
}

static char *read_key(const char *pathname, const char *key, int icase)
{
	struct stat st;
	char *map, *off, *end, *str = NULL;
	off_t size; size_t len;
	int fd, err = 0;

	fd = open(pathname, O_RDONLY);
	if (fd < 0)
		return NULL;

	if (flock(fd, LOCK_SH) < 0) {
		err = -errno;
		goto close;
	}

	if (fstat(fd, &st) < 0) {
		err = -errno;
		goto unlock;
	}

	size = st.st_size;

	map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
	if (!map || map == MAP_FAILED) {
		err = -errno;
		goto unlock;
	}

	len = strlen(key);
	off = find_key(map, size, key, len, icase);
	if (!off) {
		err = -EILSEQ;
		goto unmap;
	}

	end = strnpbrk(off, size - (off - map), "\r\n");
	if (!end) {
		err = -EILSEQ;
		goto unmap;
	}

	str = malloc(end - off - len);
	if (!str) {
		err = -EILSEQ;
		goto unmap;
	}

	memset(str, 0, end - off - len);
	strncpy(str, off + len + 1, end - off - len - 1);

unmap:
	munmap(map, size);

unlock:
	flock(fd, LOCK_UN);

close:
	close(fd);
	errno = -err;

	return str;
}

int textfile_put(const char *pathname, const char *key, const char *value)
{
	return write_key(pathname, key, value, 0);
}

int textfile_del(const char *pathname, const char *key)
{
	return write_key(pathname, key, NULL, 0);
}

char *textfile_get(const char *pathname, const char *key)
{
	return read_key(pathname, key, 0);
}

int textfile_foreach(const char *pathname, textfile_cb func, void *data)
{
	struct stat st;
	char *map, *off, *end, *key, *value;
	off_t size; size_t len;
	int fd, err = 0;

	fd = open(pathname, O_RDONLY);
	if (fd < 0)
		return -errno;

	if (flock(fd, LOCK_SH) < 0) {
		err = -errno;
		goto close;
	}

	if (fstat(fd, &st) < 0) {
		err = -errno;
		goto unlock;
	}

	size = st.st_size;

	map = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
	if (!map || map == MAP_FAILED) {
		err = -errno;
		goto unlock;
	}

	off = map;

	while (size - (off - map) > 0) {
		end = strnpbrk(off, size - (off - map), " ");
		if (!end) {
			err = -EILSEQ;
			break;
		}

		len = end - off;

		key = malloc(len + 1);
		if (!key) {
			err = -errno;
			break;
		}

		memset(key, 0, len + 1);
		memcpy(key, off, len);

		off = end + 1;

		if (size - (off - map) < 0) {
			err = -EILSEQ;
			free(key);
			break;
		}

		end = strnpbrk(off, size - (off - map), "\r\n");
		if (!end) {
			err = -EILSEQ;
			free(key);
			break;
		}

		len = end - off;

		value = malloc(len + 1);
		if (!value) {
			err = -errno;
			free(key);
			break;
		}

		memset(value, 0, len + 1);
		memcpy(value, off, len);

		func(key, value, data);

		free(key);
		free(value);

		off = end + 1;
	}

	munmap(map, size);

unlock:
	flock(fd, LOCK_UN);

close:
	close(fd);
	errno = -err;

	return 0;
}
