/*
 * Wrapper for coreutils install to preserve extended attributes.
 *
 * Copyright 2014-2026 Gentoo Authors
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.

 * 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
 * <https://www.gnu.org/licenses/>.
 */

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fnmatch.h>
#include <getopt.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <spawn.h>
#include <stdbool.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <unistd.h>

#ifndef REAL_INSTALL
#define REAL_INSTALL "/usr/bin/install"
#endif

#define DEFAULT_EXCLUDE "btrfs.*\0security.*\0trusted.*\0system.nfs4_acl"

static void *
xmalloc(size_t size)
{
	void *ret = malloc(size);
	if (ret == NULL)
		err(EXIT_FAILURE, "malloc() failed");
	return ret;
}

static void *
xrealloc(void *p, size_t size)
{
	void *ret = realloc(p, size);
	if (ret == NULL)
		err(EXIT_FAILURE, "realloc() failed");
	return ret;
}

static char *
path_join(const char *path, const char *file)
{
	size_t len_path = strlen(path);
	size_t len_file = strlen(file);
	char *ret = xmalloc(len_path + len_file + 2);

	memcpy(ret, path, len_path);
	ret[len_path] = '/';
	memcpy(ret + len_path + 1, file, len_file);
	ret[len_path + len_file + 1] = '\0';

	return ret;
}

static ssize_t
xlistxattr(const char *path, char *list, size_t size)
{
	ssize_t ret = listxattr(path, list, size);
	if (ret < 0)
		err(EXIT_FAILURE, "listxattr() failed on %s", path);
	return ret;
}

static ssize_t
xgetxattr(const char *path, char *list, char *value, size_t size)
{
	ssize_t ret = getxattr(path, list, value, size);
	if (ret < 0)
		err(EXIT_FAILURE, "getxattr() failed on %s", path);
	return ret;
}

static ssize_t
xsetxattr(const char *path, char *list, char *value, size_t size)
{
	ssize_t ret = setxattr(path, list, value, size, 0);
	if (ret < 0)
		err(EXIT_FAILURE, "setxattr() failed setting %s=%s on %s", list, value, path);
	return ret;
}

static const char *exclude; /* strings of excluded xattr names */
static size_t len_exclude;  /* length of the string of excluded xattr names */

static void
copyxattr(const char *source, const char *target)
{
	ssize_t lsize, xsize;   /* size in bytes of the list of xattrs and the values */
	char *lxattr;                  /* string of xattr names                       */
	static char *value = NULL ;    /* string of an xattr name's value             */
	static size_t value_size = 0;  /* size of the value string                    */

	lsize = listxattr(source, NULL, 0);

	if (lsize < 0) {
		warn("listxattr() failed on %s", source);
		if (errno == ENOTSUP)
			return;
		exit(EXIT_FAILURE);
	}

	/* There's no xattrs at all. */
	if (lsize == 0)
		return;

	lxattr = xmalloc(lsize);
	lsize = xlistxattr(source, lxattr, lsize);

	for (size_t i = 0;;) {
		while (i < lsize && lxattr[i] == '\0')
			++i;
		if (i >= lsize)
			break;

		for (size_t j = 0;;) {
			while (j < len_exclude && exclude[j] == '\0')
				++j;
			if (j >= len_exclude)
				break;
			if (!fnmatch(exclude + j, lxattr + i, 0))
				goto skip;
			while (j < len_exclude && exclude[j] != '\0')
				++j;
		}

		xsize = xgetxattr(source, lxattr + i, 0, 0);
		if (xsize > value_size) {
			value_size = xsize;
			value = xrealloc(value, value_size);
		}
		xgetxattr(source, lxattr + i, value, xsize);
		xsetxattr(target, lxattr + i, value, xsize);
 skip:
		while (i < lsize && lxattr[i] != '\0')
			++i;
	}

	/* No need to free(value) on return because its static and we */
	/* just keep reusing the same allocated memory on each call.  */
	free(lxattr);
}

extern char **environ;

int
main(int argc, char* argv[])
{
	/* If this is set it means Portage has switched the working directory
	 * and expects us to switch back. */
	char *portage_helper_cwd = getenv("__PORTAGE_HELPER_CWD");
	if (portage_helper_cwd && chdir(portage_helper_cwd))
		err(EXIT_FAILURE, "failed to chdir %s", portage_helper_cwd);

	argv[0] = getenv("REAL_INSTALL") ?: REAL_INSTALL;

	int r = posix_spawn(NULL, argv[0], NULL, NULL, argv, environ);
	if (r)
		errx(r == ENOENT ? 127 : 126, "failed to spawn %s: %s", argv[0], strerror(r));

	int wstatus;
	if (wait(&wstatus) < 0)
		err(EXIT_FAILURE, "failed to wait for child process");

	if (WIFSIGNALED(wstatus)) {
		/* Do the right thing and pass the signal back up.  See:
		 * http://www.cons.org/cracauer/sigint.html */
		int signum = WTERMSIG(wstatus);
		kill(getpid(), signum);
		return 128 + signum;
	}
	else {
		int estatus = WEXITSTATUS(wstatus);
		if (estatus != EXIT_SUCCESS)
			return estatus;
	}

	char *portage_xattr_exclude = getenv("PORTAGE_XATTR_EXCLUDE");
	if (portage_xattr_exclude) {
		exclude = portage_xattr_exclude;
		len_exclude = strlen(exclude);
		/* We convert exclude[] to an array of concatenated NUL terminated
		 * strings.  Also, no need to free(exclude) before we exit().
		 */
		char *pend = portage_xattr_exclude + len_exclude;
		for (char *p = portage_xattr_exclude; p != pend; ++p)
			if (isspace(*p))
				*p = '\0';
	}
	else {
		exclude = DEFAULT_EXCLUDE;
		len_exclude = sizeof(DEFAULT_EXCLUDE) - 1;
	}

	opterr = 0; /* we skip many legitimate flags, so silence any warning */

	static struct option long_options[] = {
		{           "directory",       no_argument, 0, 'd'},
		{    "target-directory", required_argument, 0, 't'},
		{               "group", required_argument, 0, 'g'},
		{                "mode", required_argument, 0, 'm'},
		{               "owner", required_argument, 0, 'o'},
		{              "suffix", required_argument, 0, 'S'},
		{             "context", optional_argument, 0, 'Z'},
		{              "backup", optional_argument, 0, 'b'},
		{                "help",       no_argument, 0,  0 },
		{                     0,                 0, 0,  0 }
	};

	bool opts_directory = false;
	bool opts_target_directory = false;
	bool target_is_directory = false;
	char *target = NULL;

	while (1) {
		int c = getopt_long(argc, argv, "dt:g:m:o:S:Zb", long_options, NULL);

		if (c == -1)
			break;

		switch (c) {
			case 0:
			case 'g':
			case 'm':
			case 'o':
			case 'S':
			case 'Z':
			case 'b':
			case '?':
				/* We skip the flags we don't care about */
				break;

			case 'd':
				opts_directory = true;
				break;

			case 't':
				opts_target_directory = true;
				target = optarg;
				break;

			default:
				err(EXIT_FAILURE, "getopt_long() failed");
		}
	}

	/* Are there enough files/directories on the cmd line to
	 * proceed?  This can happen if install is called with no
	 * arguments or with just --help.  In which case there is
	 * nothing for the parent to do.
	 */
	int first = optind;
	int last = argc - 1;
	if (first >= last)
		return EXIT_SUCCESS;

	/* If all the targets are directories, do nothing. */
	if (opts_directory)
		return EXIT_SUCCESS;

	if (!opts_target_directory) {
		struct stat s;
		target = argv[last];
		if (stat(target, &s) != 0)
			err(EXIT_FAILURE, "failed to stat %s", target);
		target_is_directory = S_ISDIR(s.st_mode);
	} else {
		/* target was set above with the -t option */
		target_is_directory = true;
	}

	if (target_is_directory) {
		/* If -t was passed, then the last argv *is*
		 * a file, so we include it for copyxattr().
		 */
		if (opts_target_directory)
			last++;

		for (int i = first; i < last; i++) {
			struct stat s;
			if (stat(argv[i], &s) != 0)
				err(EXIT_FAILURE, "failed to stat %s", argv[i]);
			/* We reproduce install's behavior and skip
			 * all extra directories on the command line
			 * that are not the final target directory.
			 */
			if (S_ISDIR(s.st_mode))
				continue;

			char *path = path_join(target, basename(argv[i]));
			copyxattr(argv[i], path);
			free(path);
		}
	} else
		copyxattr(argv[first], target);

	return EXIT_SUCCESS;
}
