/* Create a hard link relative to open directories.
   Copyright (C) 2009-2023 Free Software Foundation, Inc.

   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 3 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/>.  */

/* written by Eric Blake */

#include <config.h>

#include <unistd.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include "areadlink.h"
#include "dirname.h"
#include "eloop-threshold.h"
#include "filenamecat.h"
#include "openat-priv.h"

#if !HAVE_LINKAT || LINKAT_SYMLINK_NOTSUP

/* Create a link.  If FILE1 is a symlink, either create a hardlink to
   that symlink, or fake it by creating an identical symlink.  */
# if LINK_FOLLOWS_SYMLINKS == 0
#  define link_immediate link
# else
static int
link_immediate (char const *file1, char const *file2)
{
  char *target = areadlink (file1);
  if (target)
    {
      /* A symlink cannot be modified in-place.  Therefore, creating
         an identical symlink behaves like a hard link to a symlink,
         except for incorrect st_ino and st_nlink.  However, we must
         be careful of EXDEV.  */
      struct stat st1;
      struct stat st2;
      char *dir = mdir_name (file2);
      if (!dir)
        {
          free (target);
          errno = ENOMEM;
          return -1;
        }
      if (lstat (file1, &st1) == 0 && stat (dir, &st2) == 0)
        {
          if (st1.st_dev == st2.st_dev)
            {
              int result = symlink (target, file2);
              free (target);
              free (dir);
              return result;
            }
          free (target);
          free (dir);
          errno = EXDEV;
          return -1;
        }
      free (target);
      free (dir);
    }
  if (errno == ENOMEM)
    return -1;
  return link (file1, file2);
}
# endif /* LINK_FOLLOWS_SYMLINKS == 0 */

/* Create a link.  If FILE1 is a symlink, create a hardlink to the
   canonicalized file.  */
# if 0 < LINK_FOLLOWS_SYMLINKS
#  define link_follow link
# else
static int
link_follow (char const *file1, char const *file2)
{
  char *name = (char *) file1;
  char *target;
  int result;
  int i = __eloop_threshold ();

  /* Using realpath or canonicalize_file_name is too heavy-handed: we
     don't need an absolute name, and we don't need to resolve
     intermediate symlinks, just the basename of each iteration.  */
  while (i-- && (target = areadlink (name)))
    {
      if (IS_ABSOLUTE_FILE_NAME (target))
        {
          if (name != file1)
            free (name);
          name = target;
        }
      else
        {
          char *dir = mdir_name (name);
          if (name != file1)
            free (name);
          if (!dir)
            {
              free (target);
              errno = ENOMEM;
              return -1;
            }
          name = mfile_name_concat (dir, target, NULL);
          free (dir);
          free (target);
          if (!name)
            {
              errno = ENOMEM;
              return -1;
            }
        }
    }
  if (i < 0)
    {
      target = NULL;
      errno = ELOOP;
    }
  if (!target && errno != EINVAL)
    {
      if (name != file1)
        free (name);
      return -1;
    }
  result = link (name, file2);
  if (name != file1)
    free (name);
  return result;
}
# endif /* 0 < LINK_FOLLOWS_SYMLINKS */

/* On Solaris, link() doesn't follow symlinks by default, but does so as soon
   as a library or executable takes part in the program that has been compiled
   with "c99" or "cc -xc99=all" or "cc ... /usr/lib/values-xpg4.o ...".  */
# if LINK_FOLLOWS_SYMLINKS == -1

/* Reduce the penalty of link_immediate and link_follow by incorporating the
   knowledge that link()'s behaviour depends on the __xpg4 variable.  */
extern int __xpg4;

static int
solaris_optimized_link_immediate (char const *file1, char const *file2)
{
  if (__xpg4 == 0)
    return link (file1, file2);
  return link_immediate (file1, file2);
}

static int
solaris_optimized_link_follow (char const *file1, char const *file2)
{
  if (__xpg4 != 0)
    return link (file1, file2);
  return link_follow (file1, file2);
}

#  define link_immediate solaris_optimized_link_immediate
#  define link_follow solaris_optimized_link_follow

# endif

#endif /* !HAVE_LINKAT || LINKAT_SYMLINK_NOTSUP  */

#if !HAVE_LINKAT

/* Create a link to FILE1, in the directory open on descriptor FD1, to FILE2,
   in the directory open on descriptor FD2.  If FILE1 is a symlink, FLAG
   controls whether to dereference FILE1 first.  If possible, do it without
   changing the working directory.  Otherwise, resort to using
   save_cwd/fchdir, then rename/restore_cwd.  If either the save_cwd or
   the restore_cwd fails, then give a diagnostic and exit nonzero.  */

int
linkat (int fd1, char const *file1, int fd2, char const *file2, int flag)
{
  if (flag & ~AT_SYMLINK_FOLLOW)
    {
      errno = EINVAL;
      return -1;
    }
  return at_func2 (fd1, file1, fd2, file2,
                   flag ? link_follow : link_immediate);
}

#else /* HAVE_LINKAT */

# undef linkat

/* Create a link.  If FILE1 is a symlink, create a hardlink to the
   canonicalized file.  */

static int
linkat_follow (int fd1, char const *file1, int fd2, char const *file2)
{
  char *name = (char *) file1;
  char *target;
  int result;
  int i = __eloop_threshold ();

  /* There is no realpathat.  */
  while (i-- && (target = areadlinkat (fd1, name)))
    {
      if (IS_ABSOLUTE_FILE_NAME (target))
        {
          if (name != file1)
            free (name);
          name = target;
        }
      else
        {
          char *dir = mdir_name (name);
          if (name != file1)
            free (name);
          if (!dir)
            {
              free (target);
              errno = ENOMEM;
              return -1;
            }
          name = mfile_name_concat (dir, target, NULL);
          free (dir);
          free (target);
          if (!name)
            {
              errno = ENOMEM;
              return -1;
            }
        }
    }
  if (i < 0)
    {
      target = NULL;
      errno = ELOOP;
    }
  if (!target && errno != EINVAL)
    {
      if (name != file1)
        free (name);
      return -1;
    }
  result = linkat (fd1, name, fd2, file2, 0);
  if (name != file1)
    free (name);
  return result;
}


/* Like linkat, but guarantee that AT_SYMLINK_FOLLOW works even on
   older Linux kernels.  */

int
rpl_linkat (int fd1, char const *file1, int fd2, char const *file2, int flag)
{
  if (flag & ~AT_SYMLINK_FOLLOW)
    {
      errno = EINVAL;
      return -1;
    }

# if LINKAT_TRAILING_SLASH_BUG
  /* Reject trailing slashes on non-directories.  */
  {
    size_t len1 = strlen (file1);
    size_t len2 = strlen (file2);
    if ((len1 && file1[len1 - 1] == '/')
        || (len2 && file2[len2 - 1] == '/'))
      {
        /* Let linkat() decide whether hard-linking directories is legal.
           If fstatat() fails, then linkat() should fail for the same reason;
           if fstatat() succeeds, require a directory.  */
        struct stat st;
        if (fstatat (fd1, file1, &st, flag ? 0 : AT_SYMLINK_NOFOLLOW))
          return -1;
        if (!S_ISDIR (st.st_mode))
          {
            errno = ENOTDIR;
            return -1;
          }
      }
  }
# endif

  if (!flag)
    {
      int result = linkat (fd1, file1, fd2, file2, flag);
# if LINKAT_SYMLINK_NOTSUP
      /* OS X 10.10 has linkat() but it doesn't support
         hardlinks to symlinks.  Fallback to our emulation
         in that case.  */
      if (result == -1 && (errno == ENOTSUP || errno == EOPNOTSUPP))
        return at_func2 (fd1, file1, fd2, file2, link_immediate);
# endif
      return result;
    }

  /* Cache the information on whether the system call really works.  */
  {
    static int have_follow_really; /* 0 = unknown, 1 = yes, -1 = no */
    if (0 <= have_follow_really)
    {
      int result = linkat (fd1, file1, fd2, file2, flag);
      if (!(result == -1 && errno == EINVAL))
        {
          have_follow_really = 1;
          return result;
        }
      have_follow_really = -1;
    }
  }
  return linkat_follow (fd1, file1, fd2, file2);
}

#endif /* HAVE_LINKAT */
