/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2023-2024 Brett Sheffield <bacs@librecast.net> */

/* canonpath() was adapted from glibc realpath(3):

   Copyright (C) 1996-2020 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C 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.

   The GNU C 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 the GNU C Library; if not, see
   <https://www.gnu.org/licenses/>.  */

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
#include <errno.h>
#include <stddef.h>

char *canonpath(const char *name)
{
	char *rpath, *dest;
	const char *start, *end, *rpath_limit;
	long int path_max;

	if (name == NULL) return (errno = EINVAL), NULL;
	if (name[0] == '\0') return (errno = ENOENT), NULL;
#ifdef PATH_MAX
	path_max = PATH_MAX;
#else
	path_max = pathconf(name, _PC_PATH_MAX);
	if (path_max <= 0) path_max = 1024;
#endif
	rpath = malloc(path_max);
	if (rpath == NULL) return NULL;
	rpath_limit = rpath + path_max;

	if (name[0] != '/') {
		if (!getcwd(rpath, path_max)) {
			rpath[0] = '\0';
			goto error;
		}
#ifdef HAVE_RAWMEMCHR
		dest = rawmemchr(rpath, '\0');
#else
		dest = memchr(rpath, '\0', path_max);
#endif
	}
	else {
		rpath[0] = '/';
		dest = rpath + 1;
	}
	for (start = end = name; *start; start = end) {
		struct stat st;

		/* Skip sequence of multiple path-separators.  */
		while (*start == '/') ++start;

		/* Find end of path component.  */
		for (end = start; *end && *end != '/'; ++end);

		if (end - start == 0) break;
		else if (end - start == 1 && start[0] == '.');
		else if (end - start == 2 && start[0] == '.' && start[1] == '.') {
			/* Back up to previous component, ignore if at root already.  */
			if (dest > rpath + 1) while ((--dest)[-1] != '/');
		}
		else {
			size_t len, new_size;
			if (dest[-1] != '/') *dest++ = '/';
			if (dest + (end - start) >= rpath_limit) {
				ptrdiff_t dest_offset = dest - rpath;
				char *new_rpath;
				new_size = rpath_limit - rpath;
				if (end - start + 1 > path_max)
					new_size += end - start + 1;
				else
					new_size += path_max;
				new_rpath = (char *) realloc(rpath, new_size);
				if (new_rpath == NULL) goto error;
				rpath = new_rpath;
				rpath_limit = rpath + new_size;
				dest = rpath + dest_offset;
			}
			len = end - start;
			dest = (char *)memcpy(dest, start, len) + len;
			*dest = '\0';
			if (lstat(rpath, &st) < 0) goto error;
			if (!S_ISDIR(st.st_mode) && *end != '\0') {
				errno = ENOTDIR;
				goto error;
			}
		}
	}
	if (dest > rpath + 1 && dest[-1] == '/') --dest;
	*dest = '\0';
	return rpath;
error:
	free(rpath);
	return NULL;
}
