/*
 * Copyright (c) 2013, 2018, 2020 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * ln.c
 * Create a hard or symbolic link.
 */

#include <sys/stat.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static bool lnat(const char* source,
                 int source_dirfd,
                 const char* source_basename,
                 const char* target,
                 int target_dirfd,
                 const char* target_basename,
                 bool force,
                 bool symbolic,
                 bool physical,
                 bool no_dereference,
                 bool no_target_directory,
                 bool verbose)
{
	for ( int attempt = 0; true; attempt++ )
	{
		if ( (symbolic ?
		      symlinkat(source, target_dirfd, target_basename) :
		      linkat(source_dirfd, source_basename, target_dirfd,
		             target_basename, physical ? 0 : AT_SYMLINK_FOLLOW)) < 0 )
		{
			int error = errno;
			if ( !attempt && error == EEXIST && !no_target_directory )
			{
				struct stat target_st;
				if ( !fstatat(target_dirfd, target_basename, &target_st,
				              no_dereference ? AT_SYMLINK_NOFOLLOW : 0) &&
				     S_ISDIR(target_st.st_mode) )
				{
					int new_target_dirfd = openat(target_dirfd, target_basename,
					                              O_RDONLY | O_DIRECTORY);
					if ( new_target_dirfd < 0 )
					{
						warn("%s", target);
						return false;
					}
					char* new_target;
					if ( asprintf(&new_target, "%s/%s", target,
					              source_basename) < 0 )
						err(1, "malloc");
					const char* new_target_basename = source_basename;
					bool result =
						lnat(source, source_dirfd, source_basename,
						     new_target, new_target_dirfd, new_target_basename,
						     force, symbolic, physical, no_dereference,
						     no_target_directory, verbose);
					free(new_target);
					close(new_target_dirfd);
					return result;
				}
			}
			if ( !attempt && error == EEXIST && force )
			{
				if ( !symbolic && !strcmp(source_basename, target_basename) )
				{
					struct stat source_dirst, target_dirst;
					fstat(source_dirfd, &source_dirst);
					fstat(target_dirfd, &target_dirst);
					if ( source_dirst.st_dev == target_dirst.st_dev &&
					     source_dirst.st_ino == target_dirst.st_ino )
					{
						warnx("'%s' and '%s' are the same file", source,
						      target);
						return false;
					}
				}
				if ( unlinkat(target_dirfd, target_basename, 0) < 0 )
				{
					warn("unlink: %s", target);
					return false;
				}
				continue;
			}
			errno = error;
			warn("%s: %s -> %s", symbolic ? "symlink" : "link", source, target);
			return false;
		}
		if ( verbose )
			printf("`%s' => `%s'\n", source, target);
		return true;
	}
}

// Retains the trailing slashes unlike basename(3) so ln foo/ bar/ fails.
static const char* basename_with_slashes(const char* path)
{
	size_t offset = strlen(path);
	while ( offset && path[offset - 1] == '/' )
		offset--;
	while ( offset && path[offset - 1] != '/' )
		offset--;
	return path + offset;
}

static bool ln(const char* source,
               const char* target,
               bool force,
               bool symbolic,
               bool physical,
               bool no_dereference,
               bool no_target_directory,
               bool verbose)
{
	char* source_dup = strdup(source);
	if ( !source_dup )
		err(1, "malloc");
	const char* source_basename = basename_with_slashes(source);
	char* source_dirname = dirname(source_dup);
	int source_dirfd = symbolic ?
	                   AT_FDCWD :
	                   open(source_dirname, O_RDONLY | O_DIRECTORY);
	if ( !symbolic && source_dirfd < 0 )
	{
		warn("%s", source);
		free(source_dup);
		return false;
	}
	char* target_dup = strdup(target);
	if ( !target_dup )
		err(1, "malloc");
	const char* target_basename = basename_with_slashes(target);
	char* target_dirname = dirname(target_dup);
	int target_dirfd = open(target_dirname, O_RDONLY | O_DIRECTORY);
	if ( target_dirfd < 0 )
	{
		warn("%s", target);
		if ( symbolic )
			close(source_dirfd);
		free(source_dup);
		free(target_dup);
		return false;
	}
	bool result = lnat(source, source_dirfd, source_basename,
	                   target, target_dirfd, target_basename,
	                   force, symbolic, physical, no_dereference,
	                   no_target_directory, verbose);
	close(source_dirfd);
	close(target_dirfd);
	free(source_dup);
	free(target_dup);
	return result;
}

static bool ln_into_directory(const char* source,
                              const char* target,
                              bool force,
                              bool symbolic,
                              bool physical,
                              bool no_dereference,
                              bool verbose)
{
	char* source_copy = strdup(source);
	if ( !source_copy )
		err(1, "malloc");
	const char* base_name = basename(source_copy);
	size_t source_length = strlen(source);
	bool has_slash = source_length && source[source_length - 1] == '/';
	char* new_target;
	if ( asprintf(&new_target,
	              "%s%s%s",
	              target,
	              has_slash ? "" : "/",
	              base_name) < 0 )
		err(1, "malloc");
	free(source_copy);
	bool ret = ln(source, new_target, force, symbolic, physical, no_dereference,
	              true, verbose);
	free(new_target);
	return ret;
}

static void compact_arguments(int* argc, char*** argv)
{
	for ( int i = 0; i < *argc; i++ )
	{
		while ( i < *argc && !(*argv)[i] )
		{
			for ( int n = i; n < *argc; n++ )
				(*argv)[n] = (*argv)[n+1];
			(*argc)--;
		}
	}
}

int main(int argc, char* argv[])
{
	bool force = false;
	bool symbolic = false;
	bool physical = true;
	bool no_dereference = false;
	bool no_target_directory = false;
	bool verbose = false;

	for ( int i = 1; i < argc; i++ )
	{
		const char* arg = argv[i];
		if ( arg[0] != '-' || !arg[1] )
			continue;
		argv[i] = NULL;
		if ( !strcmp(arg, "--") )
			break;
		if ( arg[1] != '-' )
		{
			char c;
			while ( (c = *++arg) ) switch ( c )
			{
			case 'f': force = true; break;
			case 'h': no_dereference = true; break;
			case 'L': physical = false; break;
			case 'n': no_dereference = true; break;
			case 'P': physical = true; break;
			case 's': symbolic = true; break;
			case 'T': no_target_directory = true; break;
			case 'v': verbose = true; break;
			default:
				errx(1, "unknown option -- '%c'", c);
			}
		}
		else if ( !strcmp(arg, "--force") )
			force = true;
		else if ( !strcmp(arg, "--logical") )
			physical = false;
		else if ( !strcmp(arg, "--physical") )
			physical = true;
		else if ( !strcmp(arg, "--symbolic") )
			symbolic = true;
		else if ( !strcmp(arg, "--verbose") )
			verbose = true;
		else
			errx(1, "unknown option: %s", arg);
	}

	compact_arguments(&argc, &argv);

	if ( no_target_directory && argc != 3 )
		errx(1, "unexpected extra operand");

	if ( argc == 2 )
		return ln_into_directory(argv[1], ".", force, symbolic, physical,
		                         no_dereference, verbose) ? 0 : 1;

	if ( argc == 3 )
		return ln(argv[1], argv[2], force, symbolic, physical, no_dereference,
		          no_target_directory, verbose) ? 0 : 1;

	const char* target = argv[argc - 1];
	bool success = true;

	for ( int i = 1; i < argc - 1; i++ )
	{
		const char* source = argv[i];
		if ( !ln_into_directory(source, target, force, symbolic, physical,
		                        no_dereference, verbose) )
			success = false;
	}

	return success ? 0 : 1;
}
