bug-gnulib
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

linkat (was: mingw and same-inode)


From: Eric Blake
Subject: linkat (was: mingw and same-inode)
Date: Wed, 23 Sep 2009 22:19:59 -0600
User-agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.23) Gecko/20090812 Thunderbird/2.0.0.23 Mnenhy/0.7.6.666

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

According to Eric Blake on 9/23/2009 3:10 PM:
> I'm currently testing these two patches, as mingw prerequisites before I can 
> get linkat() working.  In particular, mingw is lousy at SAME_INODE, since all 
> three of [fl]stat produce st_ino == 0 for all files (then again, mingw never 
> claimed POSIX compliance!).  Code was always taking the identical-file path, 
> even for distinct files.
> 
> I'm also preparing a followup patch for coreutils usage of SAME_INODE.  
> Thoughts before I apply this?

And with this, linkat() now works.  Tested on Linux (both before and after
native linkat was added), Solaris 8-10, OpenBSD, cygwin 1.5, cygwin 1.7,
and mingw.  The only failures were on cygwin 1.7, but I have pending
patches to fix cygwin there, so I'm not worried about it.  I'm pushing
this series, and will start work on implementing 'ln -P/-L' in coreutils
tomorrow.

Eric Blake (3):
      fchdir: another mingw fix
I finally figured out the last piece of portability that was getting in
the way of openat and friends from working reliably on mingw.  Up till
now, fchdir(open(".",O_RDONLY)) worked, but not
fchdir(open("..",O_RDONLY)), because canonicalize_file_name didn't know
what to do with the directory.  But since fchdir only needs the canonical
names of directories (not regular files), and since getcwd() works on
mingw to give a canonical name, I was able to find an alternate approach
that breaks the dependency on canonicalize-lgpl altogether.

      dirname: add library-safe mdir_name
I wanted to use dirname in at_func2, but without the penalty of
xalloc_die.  This patch is sufficient for that task, although we may want
to consider a followup that splits dirname into a lighter-weight module
(possibly with portions of dirname.h under LGPL), or at least split .o
files (so that using just mdir_name from dirname.o doesn't force the
linker to require xalloc_die).

      linkat: new module
I've posted previews of at_func2 before [1], but have refined it over the
past couple of weeks.  renameat will have to wait a bit longer for fixing
rpl_rename first [2].  On (older) Linux and Solaris, linkat(,0) uses
native link, and linkat(,AT_SYMLINK_FOLLOW) manually crawls the readlink
chain (and does a better job than Solaris' /usr/xpg4/bin/link, which stops
after only one iteration, even if that is still a symlink).  On BSD,
linkat(,0) fakes the hardlink by creating an identical symlink (a trick
from coreutils' copy.c), and linkat(,AT_SYMLINK_FOLLOW) uses native link.
 Although st_nlink and st_ino are wrong, there's not much else the user
can distinguish (as there is no way to modify symlink contents).  I
suppose I should do a followup patch to use lchmod/lchown to more fully
match the attributes of the cloned symlink.

[1] http://lists.gnu.org/archive/html/bug-gnulib/2009-09/msg00097.html
[2] http://lists.gnu.org/archive/html/bug-gnulib/2009-09/msg00092.html

- --
Don't work too hard, make some time for fun as well!

Eric Blake             address@hidden
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Public key at home.comcast.net/~ericblake/eblake.gpg
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkq6828ACgkQ84KuGfSFAYCqpQCgpVEygy1V1bqTnGGgcStzMGSK
v3sAoKmVtCzN4/ll2DdJkyGA5711bBFc
=nx6R
-----END PGP SIGNATURE-----
>From 657fb89d3276465fcf756392f10aa268ca451136 Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Wed, 23 Sep 2009 20:33:40 -0600
Subject: [PATCH 1/3] fchdir: another mingw fix

canonicalize_file_name does not understand drive letters or
backslash.  The only reason openat required it was to make
fchdir get the canonical name of a directory.  But we can do
the same trick with chdir and getcwd.  With this fix,
fchdir(open("..",O_RDONLY)) finally does the right thing on mingw.

* modules/fchdir (Depends-on): Drop canonicalize-lgpl.
* lib/fchdir.c (get_name): New helper method; skips canonicalize
on mingw (where it has not yet been ported), and make it optional
elsewhere.
(_gl_register_fd): Use it.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog      |    7 +++++++
 lib/fchdir.c   |   42 +++++++++++++++++++++++++++++++++++++++++-
 modules/fchdir |    1 -
 3 files changed, 48 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index bed42c5..ee180ac 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,12 @@
 2009-09-23  Eric Blake  <address@hidden>

+       fchdir: another mingw fix
+       * modules/fchdir (Depends-on): Drop canonicalize-lgpl.
+       * lib/fchdir.c (get_name): New helper method; skips canonicalize
+       on mingw (where it has not yet been ported), and make it optional
+       elsewhere.
+       (_gl_register_fd): Use it.
+
        same-inode: make SAME_INODE tri-state, to port to mingw
        * NEWS: Mention this change.
        * lib/same-inode.h (same-inode.h): Recognize mingw limitation of
diff --git a/lib/fchdir.c b/lib/fchdir.c
index 677d442..54bbc62 100644
--- a/lib/fchdir.c
+++ b/lib/fchdir.c
@@ -33,6 +33,14 @@
 # define REPLACE_OPEN_DIRECTORY 0
 #endif

+#ifndef HAVE_CANONICALIZE_FILE_NAME
+# if GNULIB_CANONICALIZE || GNULIB_CANONICALIZE_LGPL
+#  define HAVE_CANONICALIZE_FILE_NAME 1
+# else
+#  define HAVE_CANONICALIZE_FILE_NAME 0
+# endif
+#endif
+
 /* This replacement assumes that a directory is not renamed while opened
    through a file descriptor.

@@ -78,6 +86,38 @@ ensure_dirs_slot (size_t fd)
   return true;
 }

+/* Return the canonical name of DIR in malloc'd storage.  */
+static char *
+get_name (char const *dir)
+{
+  char *result;
+  if (REPLACE_OPEN_DIRECTORY || !HAVE_CANONICALIZE_FILE_NAME)
+    {
+      /* The function canonicalize_file_name has not yet been ported
+        to mingw, with all its drive letter and backslash quirks.
+        Fortunately, getcwd is reliable in this case, but we ensure
+        we can get back to where we started before using it.  Treat
+        "." as a special case, as it is frequently encountered.  */
+      char *cwd = getcwd (NULL, 0);
+      int saved_errno;
+      if (dir[0] == '.' && dir[1] == '\0')
+       return cwd;
+      if (chdir (cwd))
+       return NULL;
+      result = chdir (dir) ? NULL : getcwd (NULL, 0);
+      saved_errno = errno;
+      chdir (cwd);
+      free (cwd);
+      errno = saved_errno;
+    }
+  else
+    {
+      /* Avoid changing the directory.  */
+      result = canonicalize_file_name (dir);
+    }
+  return result;
+}
+
 /* Hook into the gnulib replacements for open() and close() to keep track
    of the open file descriptors.  */

@@ -108,7 +148,7 @@ _gl_register_fd (int fd, const char *filename)
       || (fstat (fd, &statbuf) == 0 && S_ISDIR (statbuf.st_mode)))
     {
       if (!ensure_dirs_slot (fd)
-          || (dirs[fd].name = canonicalize_file_name (filename)) == NULL)
+          || (dirs[fd].name = get_name (filename)) == NULL)
         {
           int saved_errno = errno;
           close (fd);
diff --git a/modules/fchdir b/modules/fchdir
index 69ac3c3..5bae7d6 100644
--- a/modules/fchdir
+++ b/modules/fchdir
@@ -6,7 +6,6 @@ lib/fchdir.c
 m4/fchdir.m4

 Depends-on:
-canonicalize-lgpl
 close
 dirent
 dirfd
-- 
1.6.5.rc1


>From c6dc1761b3e928d2de0a6116cd933b3147ffd7d8 Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Wed, 9 Sep 2009 06:24:28 -0600
Subject: [PATCH 2/3] dirname: add library-safe mdir_name

A library-safe dir_name is nice, especially alongside
mfile_name_concat.  Someday, we should rearrange the .o
files so that linking in mdir_name does not suck in
xalloc_die, but for now, the only planned client of
mdir_name (at-func2) is already using xalloc_die.

* lib/dirname.h (mdir_name): New prototype.
* lib/dirname.c (dir_name): Move guts...
(mdir_name): ...to new function that avoids xalloc_die.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog     |    5 +++++
 lib/dirname.c |   28 ++++++++++++++++++++++------
 lib/dirname.h |    3 ++-
 3 files changed, 29 insertions(+), 7 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index ee180ac..0ba1bbe 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2009-09-23  Eric Blake  <address@hidden>

+       dirname: add library-safe mdir_name
+       * lib/dirname.h (mdir_name): New prototype.
+       * lib/dirname.c (dir_name): Move guts...
+       (mdir_name): ...to new function that avoids xalloc_die.
+
        fchdir: another mingw fix
        * modules/fchdir (Depends-on): Drop canonicalize-lgpl.
        * lib/fchdir.c (get_name): New helper method; skips canonicalize
diff --git a/lib/dirname.c b/lib/dirname.c
index c27e5b5..20dcaf5 100644
--- a/lib/dirname.c
+++ b/lib/dirname.c
@@ -1,7 +1,7 @@
 /* dirname.c -- return all but the last element in a file name

-   Copyright (C) 1990, 1998, 2000, 2001, 2003, 2004, 2005, 2006 Free Software
-   Foundation, Inc.
+   Copyright (C) 1990, 1998, 2000, 2001, 2003, 2004, 2005, 2006, 2009
+   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
@@ -20,6 +20,7 @@

 #include "dirname.h"

+#include <stdlib.h>
 #include <string.h>
 #include "xalloc.h"

@@ -57,9 +58,9 @@ dir_len (char const *file)
    since it has different meanings in different environments.
    In some environments the builtin `dirname' modifies its argument.

-   Return the leading directories part of FILE, allocated with xmalloc.
+   Return the leading directories part of FILE, allocated with malloc.
    Works properly even if there are trailing slashes (by effectively
-   ignoring them).  Unlike POSIX dirname(), FILE cannot be NULL.
+   ignoring them).  Return NULL on failure.

    If lstat (FILE) would succeed, then { chdir (dir_name (FILE));
    lstat (base_name (FILE)); } will access the same file.  Likewise,
@@ -68,17 +69,32 @@ dir_len (char const *file)
    to "foo" in the same directory FILE was in.  */

 char *
-dir_name (char const *file)
+mdir_name (char const *file)
 {
   size_t length = dir_len (file);
   bool append_dot = (length == 0
                     || (FILE_SYSTEM_DRIVE_PREFIX_CAN_BE_RELATIVE
                         && length == FILE_SYSTEM_PREFIX_LEN (file)
                         && file[2] != '\0' && ! ISSLASH (file[2])));
-  char *dir = xmalloc (length + append_dot + 1);
+  char *dir = malloc (length + append_dot + 1);
+  if (!dir)
+    return NULL;
   memcpy (dir, file, length);
   if (append_dot)
     dir[length++] = '.';
   dir[length] = '\0';
   return dir;
 }
+
+/* Just like mdir_name, above, except, rather than
+   returning NULL upon malloc failure, here, we report the
+   "memory exhausted" condition and exit.  */
+
+char *
+dir_name (char const *file)
+{
+  char *result = mdir_name (file);
+  if (!result)
+    xalloc_die ();
+  return result;
+}
diff --git a/lib/dirname.h b/lib/dirname.h
index f592350..90a1f0c 100644
--- a/lib/dirname.h
+++ b/lib/dirname.h
@@ -1,6 +1,6 @@
 /*  Take file names apart into directory and base names.

-    Copyright (C) 1998, 2001, 2003-2006 Free Software Foundation, Inc.
+    Copyright (C) 1998, 2001, 2003-2006, 2009 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
@@ -59,6 +59,7 @@
 # define IS_RELATIVE_FILE_NAME(F) (! IS_ABSOLUTE_FILE_NAME (F))

 char *base_name (char const *file);
+char *mdir_name (char const *file);
 char *dir_name (char const *file);
 size_t base_len (char const *file);
 size_t dir_len (char const *file);
-- 
1.6.5.rc1


>From c4194dcc56767f8f96bc005088b292f519b13910 Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Tue, 8 Sep 2009 20:47:40 -0600
Subject: [PATCH 3/3] linkat: new module

* modules/linkat: New file.
* lib/at-func2.c (at_func2): Likewise.
* lib/linkat.c (linkat): Likewise.
* m4/linkat.m4 (gl_FUNC_LINKAT): Likewise.
* lib/openat-priv.h (at_func2): Add declaration.
* m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witnesses.
* modules/unistd (Makefile.am): Substitute them.
* lib/unistd.in.h (linkat): Declare it.
* MODULES.html.sh (systems lacking POSIX:2008): Mention module.
* doc/posix-functions/linkat.texi (linkat): Likewise.
* doc/posix-functions/link.texi (link): Tweak wording.
* tests/test-link.c (main): Move guts...
* tests/test-link.h (test_link): ...into new file.
* modules/linkat-tests: New test.
* tests/test-linkat.c: Likewise.
* modules/link-tests (Files): Ship new file.
(Depends-on): Add stdbool.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog                       |   19 ++
 MODULES.html.sh                 |    4 +-
 doc/posix-functions/link.texi   |    4 +
 doc/posix-functions/linkat.texi |   10 +-
 lib/at-func2.c                  |  282 +++++++++++++++++++++++++++++++
 lib/linkat.c                    |  181 ++++++++++++++++++++
 lib/openat-priv.h               |    5 +
 lib/unistd.in.h                 |   15 ++
 m4/linkat.m4                    |   25 +++
 m4/unistd_h.m4                  |    4 +-
 modules/link-tests              |    2 +
 modules/linkat                  |   40 +++++
 modules/linkat-tests            |   16 ++
 modules/unistd                  |    2 +
 tests/test-link.c               |  111 +------------
 tests/test-link.h               |  137 +++++++++++++++
 tests/test-linkat.c             |  352 +++++++++++++++++++++++++++++++++++++++
 17 files changed, 1095 insertions(+), 114 deletions(-)
 create mode 100644 lib/at-func2.c
 create mode 100644 lib/linkat.c
 create mode 100644 m4/linkat.m4
 create mode 100644 modules/linkat
 create mode 100644 modules/linkat-tests
 create mode 100644 tests/test-link.h
 create mode 100644 tests/test-linkat.c

diff --git a/ChangeLog b/ChangeLog
index 0ba1bbe..855e69d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,24 @@
 2009-09-23  Eric Blake  <address@hidden>

+       linkat: new module
+       * modules/linkat: New file.
+       * lib/at-func2.c (at_func2): Likewise.
+       * lib/linkat.c (linkat): Likewise.
+       * m4/linkat.m4 (gl_FUNC_LINKAT): Likewise.
+       * lib/openat-priv.h (at_func2): Add declaration.
+       * m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witnesses.
+       * modules/unistd (Makefile.am): Substitute them.
+       * lib/unistd.in.h (linkat): Declare it.
+       * MODULES.html.sh (systems lacking POSIX:2008): Mention module.
+       * doc/posix-functions/linkat.texi (linkat): Likewise.
+       * doc/posix-functions/link.texi (link): Tweak wording.
+       * tests/test-link.c (main): Move guts...
+       * tests/test-link.h (test_link): ...into new file.
+       * modules/linkat-tests: New test.
+       * tests/test-linkat.c: Likewise.
+       * modules/link-tests (Files): Ship new file.
+       (Depends-on): Add stdbool.
+
        dirname: add library-safe mdir_name
        * lib/dirname.h (mdir_name): New prototype.
        * lib/dirname.c (dir_name): Move guts...
diff --git a/MODULES.html.sh b/MODULES.html.sh
index ad00ac8..42cb57c 100755
--- a/MODULES.html.sh
+++ b/MODULES.html.sh
@@ -2276,9 +2276,11 @@ func_all_modules ()
   func_module iconv_open
   func_module inet_ntop
   func_module inet_pton
+  func_module link
+  func_module linkat
+  func_module listen
   func_module locale
   func_module lseek
-  func_module listen
   func_module lstat
   func_module malloc-posix
   func_module mbsnrtowcs
diff --git a/doc/posix-functions/link.texi b/doc/posix-functions/link.texi
index c785371..c06f0a6 100644
--- a/doc/posix-functions/link.texi
+++ b/doc/posix-functions/link.texi
@@ -19,4 +19,8 @@ link

 Portability problems not fixed by Gnulib:
 @itemize
address@hidden
+When the first argument is a symlink, some platforms create a
+hard-link to what the symlink referenced, rather than to the symlink
+itself.  Use @samp{linkat} to force a particular behavior.
 @end itemize
diff --git a/doc/posix-functions/linkat.texi b/doc/posix-functions/linkat.texi
index 1c08c7e..62fc43d 100644
--- a/doc/posix-functions/linkat.texi
+++ b/doc/posix-functions/linkat.texi
@@ -4,16 +4,16 @@ linkat

 POSIX specification: 
@url{http://www.opengroup.org/onlinepubs/9699919799/functions/linkat.html}

-Gnulib module: ---
+Gnulib module: linkat

 Portability problems fixed by Gnulib:
 @itemize
address@hidden itemize
-
-Portability problems not fixed by Gnulib:
address@hidden
 @item
 This function is missing on some platforms:
 glibc 2.3.6, MacOS X 10.3, FreeBSD 6.0, NetBSD 3.0, OpenBSD 3.8, AIX
 5.1, HP-UX 11, IRIX 6.5, OSF/1 5.1, Solaris 10, Cygwin 1.5.x, mingw, Interix 
3.5, BeOS.
 @end itemize
+
+Portability problems not fixed by Gnulib:
address@hidden
address@hidden itemize
diff --git a/lib/at-func2.c b/lib/at-func2.c
new file mode 100644
index 0000000..a19b60b
--- /dev/null
+++ b/lib/at-func2.c
@@ -0,0 +1,282 @@
+/* Define an at-style functions like linkat or renameat.
+   Copyright (C) 2006, 2009 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 <http://www.gnu.org/licenses/>.  */
+
+/* written by Jim Meyering and Eric Blake */
+
+#include <config.h>
+
+#include "openat-priv.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "dirname.h" /* solely for definition of IS_ABSOLUTE_FILE_NAME */
+#include "filenamecat.h"
+#include "openat.h"
+#include "same-inode.h"
+#include "save-cwd.h"
+
+/* Call FUNC to operate on a pair of files, where FILE1 is relative to FD1,
+   and FILE2 is relative to FD2.  If possible, do it without changing the
+   working directory.  Otherwise, resort to using save_cwd/fchdir,
+   FUNC, restore_cwd (up to two times).  If either the save_cwd or the
+   restore_cwd fails, then give a diagnostic and exit nonzero.  */
+int
+at_func2 (int fd1, char const *file1,
+          int fd2, char const *file2,
+          int (*func) (char const *file1, char const *file2))
+{
+  struct saved_cwd saved_cwd;
+  int saved_errno;
+  int err;
+  char *file1_alt;
+  char *file2_alt;
+  struct stat st1;
+  struct stat st2;
+
+  /* There are 16 possible scenarios, based on whether an fd is
+     AT_FDCWD or real, and whether a file is absolute or relative:
+
+         fd1  file1 fd2  file2  action
+     0   cwd  abs   cwd  abs    direct call
+     1   cwd  abs   cwd  rel    direct call
+     2   cwd  abs   fd   abs    direct call
+     3   cwd  abs   fd   rel    chdir to fd2
+     4   cwd  rel   cwd  abs    direct call
+     5   cwd  rel   cwd  rel    direct call
+     6   cwd  rel   fd   abs    direct call
+     7   cwd  rel   fd   rel    convert file1 to abs, then case 3
+     8   fd   abs   cwd  abs    direct call
+     9   fd   abs   cwd  rel    direct call
+     10  fd   abs   fd   abs    direct call
+     11  fd   abs   fd   rel    chdir to fd2
+     12  fd   rel   cwd  abs    chdir to fd1
+     13  fd   rel   cwd  rel    convert file2 to abs, then case 12
+     14  fd   rel   fd   abs    chdir to fd1
+     15a fd1  rel   fd1  rel    chdir to fd1
+     15b fd1  rel   fd2  rel    chdir to fd1, then case 7
+
+     Try some optimizations to reduce fd to AT_FDCWD, or to at least
+     avoid converting an absolute name or doing a double chdir.  */
+
+  if ((fd1 == AT_FDCWD || IS_ABSOLUTE_FILE_NAME (file1))
+      && (fd2 == AT_FDCWD || IS_ABSOLUTE_FILE_NAME (file2)))
+    return func (file1, file2); /* Case 0-2, 4-6, 8-10.  */
+
+  /* If /proc/self/fd works, we don't need any stat or chdir.  */
+  {
+    char proc_buf1[OPENAT_BUFFER_SIZE];
+    char *proc_file1 = ((fd1 == AT_FDCWD || IS_ABSOLUTE_FILE_NAME (file1))
+                        ? (char *) file1
+                        : openat_proc_name (proc_buf1, fd1, file1));
+    if (proc_file1)
+      {
+        char proc_buf2[OPENAT_BUFFER_SIZE];
+        char *proc_file2 = ((fd2 == AT_FDCWD || IS_ABSOLUTE_FILE_NAME (file2))
+                            ? (char *) file2
+                            : openat_proc_name (proc_buf2, fd2, file2));
+        if (proc_file2)
+          {
+            int proc_result = func (proc_file1, proc_file2);
+            int proc_errno = errno;
+            if (proc_file1 != proc_buf1 && proc_file1 != file1)
+              free (proc_file1);
+            if (proc_file2 != proc_buf2 && proc_file2 != file2)
+              free (proc_file2);
+            /* If the syscall succeeds, or if it fails with an unexpected
+               errno value, then return right away.  Otherwise, fall through
+               and resort to using save_cwd/restore_cwd.  */
+            if (0 <= proc_result)
+              return proc_result;
+            if (! EXPECTED_ERRNO (proc_errno))
+              {
+                errno = proc_errno;
+                return proc_result;
+              }
+          }
+        else if (proc_file1 != proc_buf1 && proc_file1 != file1)
+          free (proc_buf1);
+      }
+  }
+
+  /* Cases 3, 7, 11-15 remain.  Time to normalize directory fds, if
+     possible.  */
+  if (IS_ABSOLUTE_FILE_NAME (file1))
+    fd1 = AT_FDCWD; /* Case 11 reduced to 3.  */
+  else if (IS_ABSOLUTE_FILE_NAME (file2))
+    fd2 = AT_FDCWD; /* Case 14 reduced to 12.  */
+
+  /* Cases 3, 7, 12, 13, 15 remain.  */
+
+  if (fd1 == AT_FDCWD) /* Cases 3, 7.  */
+    {
+      if (stat (".", &st1) == -1 || fstat (fd2, &st2) == -1)
+        return -1;
+      if (!S_ISDIR (st2.st_mode))
+        {
+          errno = ENOTDIR;
+          return -1;
+        }
+      if (SAME_INODE (st1, st2) == 1) /* Reduced to cases 1, 5.  */
+        return func (file1, file2);
+    }
+  else if (fd2 == AT_FDCWD) /* Cases 12, 13.  */
+    {
+      if (stat (".", &st2) == -1 || fstat (fd1, &st1) == -1)
+        return -1;
+      if (!S_ISDIR (st1.st_mode))
+        {
+          errno = ENOTDIR;
+          return -1;
+        }
+      if (SAME_INODE (st1, st2) == 1) /* Reduced to cases 4, 5.  */
+        return func (file1, file2);
+    }
+  else if (fd1 != fd2) /* Case 15b.  */
+    {
+      if (fstat (fd1, &st1) == -1 || fstat (fd2, &st2) == -1)
+        return -1;
+      if (!S_ISDIR (st1.st_mode) || !S_ISDIR (st2.st_mode))
+        {
+          errno = ENOTDIR;
+          return -1;
+        }
+      if (SAME_INODE (st1, st2) == 1) /* Reduced to case 15a.  */
+        {
+          fd2 = fd1;
+          if (stat (".", &st1) == 0 && SAME_INODE (st1, st2) == 1)
+            return func (file1, file2); /* Further reduced to case 5.  */
+        }
+    }
+  else /* Case 15a.  */
+    {
+      if (fstat (fd1, &st1) == -1)
+        return -1;
+      if (!S_ISDIR (st1.st_mode))
+        {
+          errno = ENOTDIR;
+          return -1;
+        }
+      if (stat (".", &st2) == 0 && SAME_INODE (st1, st2) == 1)
+        return func (file1, file2); /* Reduced to case 5.  */
+    }
+
+  /* Cases 3, 7, 12, 13, 15a, 15b remain.  With all reductions in
+     place, it is time to start changing directories.  */
+
+  if (save_cwd (&saved_cwd) != 0)
+    openat_save_fail (errno);
+
+  if (fd1 != AT_FDCWD && fd2 != AT_FDCWD && fd1 != fd2) /* Case 15b.  */
+    {
+      if (fchdir (fd1) != 0)
+        {
+          saved_errno = errno;
+          free_cwd (&saved_cwd);
+          errno = saved_errno;
+          return -1;
+        }
+      fd1 = AT_FDCWD; /* Reduced to case 7.  */
+    }
+
+  /* Cases 3, 7, 12, 13, 15a remain.  Convert one relative name to
+     absolute, if necessary.  */
+
+  file1_alt = (char *) file1;
+  file2_alt = (char *) file2;
+
+  if (fd1 == AT_FDCWD && !IS_ABSOLUTE_FILE_NAME (file1)) /* Case 7.  */
+    {
+      /* It would be nicer to use:
+         file1_alt = file_name_concat (xgetcwd (), file1, NULL);
+         but libraries should not call xalloc_die.  */
+      char *cwd = getcwd (NULL, 0);
+      if (!cwd)
+        {
+          saved_errno = errno;
+          free_cwd (&saved_cwd);
+          errno = saved_errno;
+          return -1;
+        }
+      file1_alt = mfile_name_concat (cwd, file1, NULL);
+      if (!file1_alt)
+        {
+          saved_errno = errno;
+          free (cwd);
+          free_cwd (&saved_cwd);
+          errno = saved_errno;
+          return -1;
+        }
+      free (cwd); /* Reduced to case 3.  */
+    }
+  else if (fd2 == AT_FDCWD && !IS_ABSOLUTE_FILE_NAME (file2)) /* Case 13.  */
+    {
+      char *cwd = getcwd (NULL, 0);
+      if (!cwd)
+        {
+          saved_errno = errno;
+          free_cwd (&saved_cwd);
+          errno = saved_errno;
+          return -1;
+        }
+      file2_alt = mfile_name_concat (cwd, file2, NULL);
+      if (!file2_alt)
+        {
+          saved_errno = errno;
+          free (cwd);
+          free_cwd (&saved_cwd);
+          errno = saved_errno;
+          return -1;
+        }
+      free (cwd); /* Reduced to case 12.  */
+    }
+
+  /* Cases 3, 12, 15a remain.  Change to the correct directory.  */
+  if (fchdir (fd1 == AT_FDCWD ? fd2 : fd1) != 0)
+    {
+      saved_errno = errno;
+      free_cwd (&saved_cwd);
+      if (file1 != file1_alt)
+        free (file1_alt);
+      else if (file2 != file2_alt)
+        free (file2_alt);
+      errno = saved_errno;
+      return -1;
+    }
+
+  /* Finally safe to perform the user's function, then clean up.  */
+
+  err = func (file1_alt, file2_alt);
+  saved_errno = (err < 0 ? errno : 0);
+
+  if (file1 != file1_alt)
+    free (file1_alt);
+  else if (file2 != file2_alt)
+    free (file2_alt);
+
+  if (restore_cwd (&saved_cwd) != 0)
+    openat_restore_fail (errno);
+
+  free_cwd (&saved_cwd);
+
+  if (saved_errno)
+    errno = saved_errno;
+  return err;
+}
+#undef CALL_FUNC
+#undef FUNC_RESULT
diff --git a/lib/linkat.c b/lib/linkat.c
new file mode 100644
index 0000000..bda0627
--- /dev/null
+++ b/lib/linkat.c
@@ -0,0 +1,181 @@
+/* Create a hard link relative to open directories.
+   Copyright (C) 2009 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 <http://www.gnu.org/licenses/>.  */
+
+/* written by Eric Blake */
+
+#include <config.h>
+
+#include <unistd.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sys/stat.h>
+
+#include "areadlink.h"
+#include "dirname.h"
+#include "filenamecat.h"
+#include "openat-priv.h"
+
+#if HAVE_SYS_PARAM_H
+# include <sys/param.h>
+#endif
+#ifndef MAXSYMLINKS
+# ifdef SYMLOOP_MAX
+#  define MAXSYMLINKS SYMLOOP_MAX
+# else
+#  define MAXSYMLINKS 20
+# endif
+#endif
+
+/* 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);
+              int saved_errno = errno;
+              free (target);
+              free (dir);
+              errno = saved_errno;
+              return result;
+            }
+          free (target);
+          free (dir);
+          errno = EXDEV;
+          return -1;
+        }
+      free (target);
+      free (dir);
+    }
+  if (errno == ENOMEM)
+    return -1;
+  return link (file1, file2);
+}
+#endif
+
+/* 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 = MAXSYMLINKS;
+
+  /* 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)
+        {
+          int saved_errno = errno;
+          free (name);
+          errno = saved_errno;
+        }
+      return -1;
+    }
+  result = link (name, file2);
+  if (name != file1)
+    {
+      int saved_errno = errno;
+      free (name);
+      errno = saved_errno;
+    }
+  return result;
+}
+#endif
+
+/* 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);
+}
diff --git a/lib/openat-priv.h b/lib/openat-priv.h
index fa286b5..53016a1 100644
--- a/lib/openat-priv.h
+++ b/lib/openat-priv.h
@@ -36,4 +36,9 @@ char *openat_proc_name (char buf[OPENAT_BUFFER_SIZE], int fd, 
char const *file);
    || (Errno) == ENOSYS /* Solaris 8 */                \
    || (Errno) == EOPNOTSUPP /* FreeBSD */)

+/* Wrapper function shared among linkat and renameat.  */
+int at_func2 (int fd1, char const *file1,
+             int fd2, char const *file2,
+             int (*func) (char const *file1, char const *file2));
+
 #endif /* _GL_HEADER_OPENAT_PRIV */
diff --git a/lib/unistd.in.h b/lib/unistd.in.h
index 600f224..8a96e79 100644
--- a/lib/unistd.in.h
+++ b/lib/unistd.in.h
@@ -581,6 +581,21 @@ extern int link (const char *path1, const char *path2);
      link (path1, path2))
 #endif

+#if @GNULIB_LINKAT@
+/* Create a new hard link for an existing file, relative to two
+   directories.  FLAG controls whether symlinks are followed.
+   Return 0 if successful, otherwise -1 and errno set.  */
+# if address@hidden@
+extern int linkat (int fd1, const char *path1, int fd2, const char *path2,
+                  int flag);
+# endif
+#elif defined GNULIB_POSIXCHECK
+# undef linkat
+# define link(f1,path1,f2,path2,f)             \
+    (GL_LINK_WARNING ("linkat is unportable - " \
+                      "use gnulib module linkat for portability"), \
+     linkat (f1, path1, f2, path2,f))
+#endif

 #if @GNULIB_LSEEK@
 # if @REPLACE_LSEEK@
diff --git a/m4/linkat.m4 b/m4/linkat.m4
new file mode 100644
index 0000000..be68c5f
--- /dev/null
+++ b/m4/linkat.m4
@@ -0,0 +1,25 @@
+# serial 1
+# See if we need to provide linkat replacement.
+
+dnl Copyright (C) 2009 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+# Written by Eric Blake.
+
+AC_DEFUN([gl_FUNC_LINKAT],
+[
+  AC_REQUIRE([gl_FUNC_OPENAT])
+  AC_REQUIRE([gl_FUNC_LINK])
+  AC_REQUIRE([gl_FUNC_LINK_FOLLOWS_SYMLINK])
+  AC_REQUIRE([gl_UNISTD_H_DEFAULTS])
+  AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS])
+  AC_CHECK_FUNCS_ONCE([linkat symlink])
+  AC_CHECK_HEADERS_ONCE([sys/param.h])
+  if test $ac_cv_func_linkat = no; then
+    HAVE_LINKAT=0
+    AC_LIBOBJ([linkat])
+    AC_LIBOBJ([at-func2])
+  fi
+])
diff --git a/m4/unistd_h.m4 b/m4/unistd_h.m4
index 2334582..16daed8 100644
--- a/m4/unistd_h.m4
+++ b/m4/unistd_h.m4
@@ -1,4 +1,4 @@
-# unistd_h.m4 serial 29
+# unistd_h.m4 serial 30
 dnl Copyright (C) 2006-2009 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -52,6 +52,7 @@ AC_DEFUN([gl_UNISTD_H_DEFAULTS],
   GNULIB_GETUSERSHELL=0;     AC_SUBST([GNULIB_GETUSERSHELL])
   GNULIB_LCHOWN=0;           AC_SUBST([GNULIB_LCHOWN])
   GNULIB_LINK=0;             AC_SUBST([GNULIB_LINK])
+  GNULIB_LINKAT=0;           AC_SUBST([GNULIB_LINKAT])
   GNULIB_LSEEK=0;            AC_SUBST([GNULIB_LSEEK])
   GNULIB_PIPE2=0;            AC_SUBST([GNULIB_PIPE2])
   GNULIB_READLINK=0;         AC_SUBST([GNULIB_READLINK])
@@ -79,6 +80,7 @@ AC_DEFUN([gl_UNISTD_H_DEFAULTS],
   HAVE_GETPAGESIZE=1;     AC_SUBST([HAVE_GETPAGESIZE])
   HAVE_GETUSERSHELL=1;    AC_SUBST([HAVE_GETUSERSHELL])
   HAVE_LINK=1;            AC_SUBST([HAVE_LINK])
+  HAVE_LINKAT=1;          AC_SUBST([HAVE_LINKAT])
   HAVE_PIPE2=1;           AC_SUBST([HAVE_PIPE2])
   HAVE_READLINK=1;        AC_SUBST([HAVE_READLINK])
   HAVE_READLINKAT=1;      AC_SUBST([HAVE_READLINKAT])
diff --git a/modules/link-tests b/modules/link-tests
index ca61deb..d8e7b1a 100644
--- a/modules/link-tests
+++ b/modules/link-tests
@@ -1,8 +1,10 @@
 Files:
+tests/test-link.h
 tests/test-link.c

 Depends-on:
 errno
+stdbool
 sys_stat

 configure.ac:
diff --git a/modules/linkat b/modules/linkat
new file mode 100644
index 0000000..8d9dec3
--- /dev/null
+++ b/modules/linkat
@@ -0,0 +1,40 @@
+Description:
+linkat(): create a hard link, relative to two directories
+
+Files:
+lib/at-func2.c
+lib/linkat.c
+m4/linkat.m4
+
+Depends-on:
+areadlink
+dirname
+errno
+extensions
+fcntl-h
+filenamecat
+openat
+link
+link-follow
+lstat
+readlink
+same-inode
+stpcpy
+symlink
+unistd
+
+configure.ac:
+gl_FUNC_LINKAT
+gl_UNISTD_MODULE_INDICATOR([linkat])
+
+Makefile.am:
+
+Include:
+<fcntl.h>
+<unistd.h>
+
+License:
+GPL
+
+Maintainer:
+Jim Meyering, Eric Blake
diff --git a/modules/linkat-tests b/modules/linkat-tests
new file mode 100644
index 0000000..9fb6505
--- /dev/null
+++ b/modules/linkat-tests
@@ -0,0 +1,16 @@
+Files:
+tests/test-linkat.c
+
+Depends-on:
+areadlink-with-size
+filenamecat
+progname
+same-inode
+xgetcwd
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-linkat
+check_PROGRAMS += test-linkat
+test_linkat_LDADD = $(LDADD) @LIBINTL@
diff --git a/modules/unistd b/modules/unistd
index 875efb0..d21a204 100644
--- a/modules/unistd
+++ b/modules/unistd
@@ -45,6 +45,7 @@ unistd.h: unistd.in.h
              -e 's|@''GNULIB_GETUSERSHELL''@|$(GNULIB_GETUSERSHELL)|g' \
              -e 's|@''GNULIB_LCHOWN''@|$(GNULIB_LCHOWN)|g' \
              -e 's|@''GNULIB_LINK''@|$(GNULIB_LINK)|g' \
+             -e 's|@''GNULIB_LINKAT''@|$(GNULIB_LINKAT)|g' \
              -e 's|@''GNULIB_LSEEK''@|$(GNULIB_LSEEK)|g' \
              -e 's|@''GNULIB_PIPE2''@|$(GNULIB_PIPE2)|g' \
              -e 's|@''GNULIB_READLINK''@|$(GNULIB_READLINK)|g' \
@@ -71,6 +72,7 @@ unistd.h: unistd.in.h
              -e 's|@''HAVE_GETPAGESIZE''@|$(HAVE_GETPAGESIZE)|g' \
              -e 's|@''HAVE_GETUSERSHELL''@|$(HAVE_GETUSERSHELL)|g' \
              -e 's|@''HAVE_LINK''@|$(HAVE_LINK)|g' \
+             -e 's|@''HAVE_LINKAT''@|$(HAVE_LINKAT)|g' \
              -e 's|@''HAVE_PIPE2''@|$(HAVE_PIPE2)|g' \
              -e 's|@''HAVE_READLINK''@|$(HAVE_READLINK)|g' \
              -e 's|@''HAVE_READLINKAT''@|$(HAVE_READLINKAT)|g' \
diff --git a/tests/test-link.c b/tests/test-link.c
index e09a0bb..a77ffe7 100644
--- a/tests/test-link.c
+++ b/tests/test-link.c
@@ -20,6 +20,7 @@

 #include <errno.h>
 #include <fcntl.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -40,117 +41,13 @@

 #define BASE "test-link.t"

+#include "test-link.h"
+
 int
 main (int argc, char **argv)
 {
-  int fd;
-  int ret;
-
   /* Remove any garbage left from previous partial runs.  */
   ASSERT (system ("rm -rf " BASE "*") == 0);

-  /* Create first file.  */
-  fd = open (BASE "a", O_CREAT | O_EXCL | O_WRONLY, 0600);
-  ASSERT (0 <= fd);
-  ASSERT (write (fd, "hello", 5) == 5);
-  ASSERT (close (fd) == 0);
-
-  /* Not all file systems support link.  Mingw doesn't have reliable
-     st_nlink on hard links, but our implementation does fail with
-     EPERM on poor file systems, and we can detect the inferior stat()
-     via st_ino.  Cygwin 1.5.x copies rather than links files on those
-     file systems, but there, st_nlink and st_ino are reliable.  */
-  ret = link (BASE "a", BASE "b");
-  if (!ret)
-  {
-    struct stat st;
-    ASSERT (stat (BASE "b", &st) == 0);
-    if (st.st_ino && st.st_nlink != 2)
-      {
-        ASSERT (unlink (BASE "b") == 0);
-        errno = EPERM;
-        ret = -1;
-      }
-  }
-  if (ret == -1)
-    {
-      /* If the device does not support hard links, errno is
-        EPERM on Linux, EOPNOTSUPP on FreeBSD.  */
-      switch (errno)
-       {
-       case EPERM:
-       case EOPNOTSUPP:
-          fputs ("skipping test: "
-                 "hard links are not supported on this file system\n", stderr);
-          ASSERT (unlink (BASE "a") == 0);
-         return 77;
-       default:
-         perror ("link");
-         return 1;
-       }
-    }
-  ASSERT (ret == 0);
-
-  /* Now, for some behavior tests.  Modify the contents of 'b', and
-     ensure that 'a' can see it, both while 'b' exists and after.  */
-  fd = open (BASE "b", O_APPEND | O_WRONLY);
-  ASSERT (0 <= fd);
-  ASSERT (write (fd, "world", 5) == 5);
-  ASSERT (close (fd) == 0);
-  {
-    char buf[11] = { 0 };
-    fd = open (BASE "a", O_RDONLY);
-    ASSERT (0 <= fd);
-    ASSERT (read (fd, buf, 10) == 10);
-    ASSERT (strcmp (buf, "helloworld") == 0);
-    ASSERT (close (fd) == 0);
-    ASSERT (unlink (BASE "b") == 0);
-    fd = open (BASE "a", O_RDONLY);
-    ASSERT (0 <= fd);
-    ASSERT (read (fd, buf, 10) == 10);
-    ASSERT (strcmp (buf, "helloworld") == 0);
-    ASSERT (close (fd) == 0);
-  }
-
-  /* Test for various error conditions.  Assumes hard links to
-     directories are not permitted.  */
-  ASSERT (mkdir (BASE "d", 0700) == 0);
-  errno = 0;
-  ASSERT (link (BASE "a", ".") == -1);
-  ASSERT (errno == EEXIST || errno == EINVAL);
-  errno = 0;
-  ASSERT (link (BASE "a", BASE "a") == -1);
-  ASSERT (errno == EEXIST);
-  ASSERT (link (BASE "a", BASE "b") == 0);
-  errno = 0;
-  ASSERT (link (BASE "a", BASE "b") == -1);
-  ASSERT (errno == EEXIST);
-  errno = 0;
-  ASSERT (link (BASE "a", BASE "d") == -1);
-  ASSERT (errno == EEXIST);
-  errno = 0;
-  ASSERT (link (BASE "c", BASE "e") == -1);
-  ASSERT (errno == ENOENT);
-  errno = 0;
-  ASSERT (link (BASE "a", BASE "c/.") == -1);
-  ASSERT (errno == ENOENT);
-  errno = 0;
-  ASSERT (link (BASE "a/", BASE "c") == -1);
-  ASSERT (errno == ENOTDIR);
-  errno = 0;
-  ASSERT (link (BASE "a", BASE "c/") == -1);
-  ASSERT (errno == ENOTDIR || errno == ENOENT);
-  errno = 0;
-  ASSERT (link (BASE "d", BASE "c") == -1);
-  ASSERT (errno == EPERM || errno == EACCES);
-
-  /* Clean up.  */
-  ASSERT (unlink (BASE "a") == 0);
-  ASSERT (unlink (BASE "b") == 0);
-  errno = 0;
-  ASSERT (unlink (BASE "c") == -1);
-  ASSERT (errno == ENOENT);
-  ASSERT (rmdir (BASE "d") == 0);
-
-  return 0;
+  return test_link (link, true);
 }
diff --git a/tests/test-link.h b/tests/test-link.h
new file mode 100644
index 0000000..9ce1894
--- /dev/null
+++ b/tests/test-link.h
@@ -0,0 +1,137 @@
+/* Test of link() function.
+   Copyright (C) 2009 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 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 <http://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+/* This file is designed to test both link(a,b) and
+   linkat(AT_FDCWD,a,AT_FDCWD,b).  FUNC is the function to test.
+   Assumes that BASE and ASSERT are already defined, and that
+   appropriate headers are already included.  If PRINT, warn before
+   skipping symlink tests with status 77.  */
+
+static int
+test_link (int (*func) (char const *, char const *), bool print)
+{
+  int fd;
+  int ret;
+
+  /* Create first file.  */
+  fd = open (BASE "a", O_CREAT | O_EXCL | O_WRONLY, 0600);
+  ASSERT (0 <= fd);
+  ASSERT (write (fd, "hello", 5) == 5);
+  ASSERT (close (fd) == 0);
+
+  /* Not all file systems support link.  Mingw doesn't have reliable
+     st_nlink on hard links, but our implementation does fail with
+     EPERM on poor file systems, and we can detect the inferior stat()
+     via st_ino.  Cygwin 1.5.x copies rather than links files on those
+     file systems, but there, st_nlink and st_ino are reliable.  */
+  ret = func (BASE "a", BASE "b");
+  if (!ret)
+  {
+    struct stat st;
+    ASSERT (stat (BASE "b", &st) == 0);
+    if (st.st_ino && st.st_nlink != 2)
+      {
+        ASSERT (unlink (BASE "b") == 0);
+        errno = EPERM;
+        ret = -1;
+      }
+  }
+  if (ret == -1)
+    {
+      /* If the device does not support hard links, errno is
+        EPERM on Linux, EOPNOTSUPP on FreeBSD.  */
+      switch (errno)
+       {
+       case EPERM:
+       case EOPNOTSUPP:
+          if (print)
+            fputs ("skipping test: "
+                   "hard links not supported on this file system\n",
+                   stderr);
+          ASSERT (unlink (BASE "a") == 0);
+         return 77;
+       default:
+         perror ("link");
+         return 1;
+       }
+    }
+  ASSERT (ret == 0);
+
+  /* Now, for some behavior tests.  Modify the contents of 'b', and
+     ensure that 'a' can see it, both while 'b' exists and after.  */
+  fd = open (BASE "b", O_APPEND | O_WRONLY);
+  ASSERT (0 <= fd);
+  ASSERT (write (fd, "world", 5) == 5);
+  ASSERT (close (fd) == 0);
+  {
+    char buf[11] = { 0 };
+    fd = open (BASE "a", O_RDONLY);
+    ASSERT (0 <= fd);
+    ASSERT (read (fd, buf, 10) == 10);
+    ASSERT (strcmp (buf, "helloworld") == 0);
+    ASSERT (close (fd) == 0);
+    ASSERT (unlink (BASE "b") == 0);
+    fd = open (BASE "a", O_RDONLY);
+    ASSERT (0 <= fd);
+    ASSERT (read (fd, buf, 10) == 10);
+    ASSERT (strcmp (buf, "helloworld") == 0);
+    ASSERT (close (fd) == 0);
+  }
+
+  /* Test for various error conditions.  Assumes hard links to
+     directories are not permitted.  */
+  ASSERT (mkdir (BASE "d", 0700) == 0);
+  errno = 0;
+  ASSERT (func (BASE "a", ".") == -1);
+  ASSERT (errno == EEXIST || errno == EINVAL);
+  errno = 0;
+  ASSERT (func (BASE "a", BASE "a") == -1);
+  ASSERT (errno == EEXIST);
+  ASSERT (func (BASE "a", BASE "b") == 0);
+  errno = 0;
+  ASSERT (func (BASE "a", BASE "b") == -1);
+  ASSERT (errno == EEXIST);
+  errno = 0;
+  ASSERT (func (BASE "a", BASE "d") == -1);
+  ASSERT (errno == EEXIST);
+  errno = 0;
+  ASSERT (func (BASE "c", BASE "e") == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "a", BASE "c/.") == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "a/", BASE "c") == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (func (BASE "a", BASE "c/") == -1);
+  ASSERT (errno == ENOTDIR || errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "d", BASE "c") == -1);
+  ASSERT (errno == EPERM || errno == EACCES);
+
+  /* Clean up.  */
+  ASSERT (unlink (BASE "a") == 0);
+  ASSERT (unlink (BASE "b") == 0);
+  errno = 0;
+  ASSERT (unlink (BASE "c") == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (rmdir (BASE "d") == 0);
+
+  return 0;
+}
diff --git a/tests/test-linkat.c b/tests/test-linkat.c
new file mode 100644
index 0000000..afb1799
--- /dev/null
+++ b/tests/test-linkat.c
@@ -0,0 +1,352 @@
+/* Tests of linkat.
+   Copyright (C) 2009 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 <http://www.gnu.org/licenses/>.  */
+
+/* Written by Eric Blake <address@hidden>, 2009.  */
+
+#include <config.h>
+
+#include <unistd.h>
+
+#include <fcntl.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "areadlink.h"
+#include "filenamecat.h"
+#include "same-inode.h"
+#include "xgetcwd.h"
+
+#define ASSERT(expr) \
+  do                                                                         \
+    {                                                                        \
+      if (!(expr))                                                           \
+       {                                                                    \
+         fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__);  \
+         fflush (stderr);                                                   \
+         abort ();                                                          \
+       }                                                                    \
+    }                                                                        \
+  while (0)
+
+#define BASE "test-linkat.t"
+
+#include "test-link.h"
+
+static int dfd1 = AT_FDCWD;
+static int dfd2 = AT_FDCWD;
+static int flag = AT_SYMLINK_FOLLOW;
+
+/* Wrapper to test linkat like link.  */
+static int
+do_link (char const *name1, char const *name2)
+{
+  return linkat (dfd1, name1, dfd2, name2, flag);
+}
+
+/* Wrapper to see if two symlinks act the same.  */
+static void
+check_same_link (char const *name1, char const *name2)
+{
+  struct stat st1;
+  struct stat st2;
+  char *contents1;
+  char *contents2;
+  ASSERT (lstat (name1, &st1) == 0);
+  ASSERT (lstat (name2, &st2) == 0);
+  contents1 = areadlink_with_size (name1, st1.st_size);
+  contents2 = areadlink_with_size (name2, st2.st_size);
+  ASSERT (contents1);
+  ASSERT (contents2);
+  ASSERT (strcmp (contents1, contents2) == 0);
+  if (!LINK_FOLLOWS_SYMLINKS)
+    ASSERT (SAME_INODE (st1, st2));
+  free (contents1);
+  free (contents2);
+}
+
+int
+main ()
+{
+  int i;
+  int dfd;
+  char *cwd;
+  int result;
+
+  /* Clean up any trash from prior testsuite runs.  */
+  ASSERT (system ("rm -rf " BASE "*") == 0);
+
+  /* Test basic link functionality, without mentioning symlinks.  */
+  {
+    result = test_link (do_link, false);
+    dfd1 = open (".", O_RDONLY);
+    ASSERT (0 <= dfd1);
+    ASSERT (test_link (do_link, false) == result);
+    dfd2 = dfd1;
+    ASSERT (test_link (do_link, false) == result);
+    dfd1 = AT_FDCWD;
+    ASSERT (test_link (do_link, false) == result);
+    flag = 0;
+    ASSERT (test_link (do_link, false) == result);
+    dfd1 = dfd2;
+    ASSERT (test_link (do_link, false) == result);
+    dfd2 = AT_FDCWD;
+    ASSERT (test_link (do_link, false) == result);
+    ASSERT (close (dfd1) == 0);
+    dfd1 = AT_FDCWD;
+    ASSERT (test_link (do_link, false) == result);
+  }
+
+  /* Create locations to manipulate.  */
+  ASSERT (mkdir (BASE "sub1", 0700) == 0);
+  ASSERT (mkdir (BASE "sub2", 0700) == 0);
+  dfd = creat (BASE "00", 0600);
+  ASSERT (0 <= dfd);
+  ASSERT (close (dfd) == 0);
+  cwd = xgetcwd ();
+
+  dfd = open (BASE "sub1", O_RDONLY);
+  ASSERT (0 <= dfd);
+  ASSERT (chdir (BASE "sub2") == 0);
+
+  /* There are 16 possible scenarios, based on whether an fd is
+     AT_FDCWD or real, whether a file is absolute or relative, coupled
+     with whether flag is set for 32 iterations.
+
+     To ensure that we test all of the code paths (rather than
+     triggering early normalization optimizations), we use a loop to
+     repeatedly rename a file in the parent directory, use an fd open
+     on subdirectory 1, all while executing in subdirectory 2; all
+     relative names are thus given with a leading "../".  Finally, the
+     last scenario (two relative paths given, neither one AT_FDCWD)
+     has two paths, based on whether the two fds are equivalent, so we
+     do the other variant after the loop.  */
+  for (i = 0; i < 32; i++)
+    {
+      int flag = (i & 0x10 ? AT_SYMLINK_FOLLOW : 0);
+      int fd1 = (i & 8) ? dfd : AT_FDCWD;
+      char *file1 = file_name_concat ((i & 4) ? ".." : cwd, BASE "xx", NULL);
+      int fd2 = (i & 2) ? dfd : AT_FDCWD;
+      char *file2 = file_name_concat ((i & 1) ? ".." : cwd, BASE "xx", NULL);
+
+      ASSERT (sprintf (strchr (file1, '\0') - 2, "%02d", i) == 2);
+      ASSERT (sprintf (strchr (file2, '\0') - 2, "%02d", i + 1) == 2);
+      ASSERT (linkat (fd1, file1, fd2, file2, flag) == 0);
+      ASSERT (unlinkat (fd1, file1, 0) == 0);
+      free (file1);
+      free (file2);
+    }
+  dfd2 = open ("..", O_RDONLY);
+  ASSERT (0 <= dfd2);
+  ASSERT (linkat (dfd, "../" BASE "32", dfd2, BASE "33", 0) == 0);
+  ASSERT (linkat (dfd, "../" BASE "33", dfd2, BASE "34",
+                  AT_SYMLINK_FOLLOW) == 0);
+  ASSERT (close (dfd2) == 0);
+
+  /* Now we change back to the parent directory, and set dfd to ".",
+     in order to test behavior on symlinks.  */
+  ASSERT (chdir ("..") == 0);
+  ASSERT (close (dfd) == 0);
+  if (symlink (BASE "sub1", BASE "link1"))
+    {
+      ASSERT (unlink (BASE "32") == 0);
+      ASSERT (unlink (BASE "33") == 0);
+      ASSERT (unlink (BASE "34") == 0);
+      ASSERT (rmdir (BASE "sub1") == 0);
+      ASSERT (rmdir (BASE "sub2") == 0);
+      free (cwd);
+      fputs ("skipping test: symlinks not supported on this filesystem\n",
+             stderr);
+      return result;
+    }
+  dfd = open (".", O_RDONLY);
+  ASSERT (0 <= dfd);
+  ASSERT (symlink (BASE "34", BASE "link2") == 0);
+  ASSERT (symlink (BASE "link3", BASE "link3") == 0);
+  ASSERT (symlink (BASE "nowhere", BASE "link4") == 0);
+
+  /* Link cannot overwrite existing files.  */
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "sub1", 0) == -1);
+  ASSERT (errno == EEXIST);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1/", dfd, BASE "sub1", 0) == -1);
+  ASSERT (errno == EEXIST || errno == EPERM || errno == EACCES);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "sub1/", 0) == -1);
+  ASSERT (errno == EEXIST);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "sub1",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST || errno == EPERM || errno == EACCES);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1/", dfd, BASE "sub1",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST || errno == EPERM || errno == EACCES);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "sub1/",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST || errno == EPERM || errno == EACCES);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link2", 0) == -1);
+  ASSERT (errno == EEXIST);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link2",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST || errno == EPERM || errno == EACCES);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link3", 0) == -1);
+  ASSERT (errno == EEXIST || errno == ELOOP);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link3",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST || errno == EPERM || errno == EACCES
+          || errno == ELOOP);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link2", dfd, BASE "link3", 0) == -1);
+  ASSERT (errno == EEXIST || errno == ELOOP);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link2", dfd, BASE "link3",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST || errno == ELOOP);
+
+  /* AT_SYMLINK_FOLLOW only follows first argument, not second.  */
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link4", 0) == -1);
+  ASSERT (errno == EEXIST);
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link4",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST || errno == EPERM || errno == EACCES);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "34", dfd, BASE "link4", 0) == -1);
+  ASSERT (errno == EEXIST);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "34", dfd, BASE "link4", AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EEXIST);
+
+  /* Trailing slash handling.  */
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link2/", dfd, BASE "link5", 0) == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link2/", dfd, BASE "link5",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == ENOTDIR);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link3/", dfd, BASE "link5", 0) == -1);
+  ASSERT (errno == ELOOP);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link3/", dfd, BASE "link5",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == ELOOP);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link4/", dfd, BASE "link5", 0) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link4/", dfd, BASE "link5",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == ENOENT);
+
+  /* Check for hard links to symlinks.  */
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link5", 0) == 0);
+  check_same_link (BASE "link1", BASE "link5");
+  ASSERT (unlink (BASE "link5") == 0);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link1", dfd, BASE "link5",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == EPERM || errno == EACCES);
+  ASSERT (linkat (dfd, BASE "link2", dfd, BASE "link5", 0) == 0);
+  check_same_link (BASE "link2", BASE "link5");
+  ASSERT (unlink (BASE "link5") == 0);
+  ASSERT (linkat (dfd, BASE "link2", dfd, BASE "file", AT_SYMLINK_FOLLOW) == 
0);
+  errno = 0;
+  ASSERT (areadlink (BASE "file") == NULL);
+  ASSERT (errno == EINVAL);
+  ASSERT (unlink (BASE "file") == 0);
+  ASSERT (linkat (dfd, BASE "link3", dfd, BASE "link5", 0) == 0);
+  check_same_link (BASE "link3", BASE "link5");
+  ASSERT (unlink (BASE "link5") == 0);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link3", dfd, BASE "link5",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == ELOOP);
+  ASSERT (linkat (dfd, BASE "link4", dfd, BASE "link5", 0) == 0);
+  check_same_link (BASE "link4", BASE "link5");
+  ASSERT (unlink (BASE "link5") == 0);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link4", dfd, BASE "link5",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == ENOENT);
+
+  /* Check that symlink to symlink to file is followed all the way.  */
+  ASSERT (symlink (BASE "link2", BASE "link5") == 0);
+  ASSERT (linkat (dfd, BASE "link5", dfd, BASE "link6", 0) == 0);
+  check_same_link (BASE "link5", BASE "link6");
+  ASSERT (unlink (BASE "link6") == 0);
+  ASSERT (linkat (dfd, BASE "link5", dfd, BASE "file", AT_SYMLINK_FOLLOW) == 
0);
+  errno = 0;
+  ASSERT (areadlink (BASE "file") == NULL);
+  ASSERT (errno == EINVAL);
+  ASSERT (unlink (BASE "link5") == 0);
+  ASSERT (symlink (BASE "link3", BASE "link5") == 0);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link5", dfd, BASE "file",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == ELOOP);
+  ASSERT (unlink (BASE "link5") == 0);
+  ASSERT (symlink (BASE "link4", BASE "link5") == 0);
+  errno = 0;
+  ASSERT (linkat (dfd, BASE "link5", dfd, BASE "file",
+                  AT_SYMLINK_FOLLOW) == -1);
+  ASSERT (errno == ENOENT);
+
+  /* Now for some real fun with directory crossing.  */
+  ASSERT (symlink (cwd, BASE "sub1/link") == 0);
+  ASSERT (symlink (".././/" BASE "sub1/link/" BASE "link2",
+                   BASE "sub2/link") == 0);
+  ASSERT (close (dfd) == 0);
+  dfd = open (BASE "sub1", O_RDONLY);
+  ASSERT (0 <= dfd);
+  dfd2 = open (BASE "sub2", O_RDONLY);
+  ASSERT (0 < dfd2);
+  ASSERT (linkat (dfd, "../" BASE "sub2/link", dfd2, "./..//" BASE "sub1/file",
+              AT_SYMLINK_FOLLOW) == 0);
+  errno = 0;
+  ASSERT (areadlink (BASE "sub1/file") == NULL);
+  ASSERT (errno == EINVAL);
+
+  /* Cleanup.  */
+  ASSERT (close (dfd) == 0);
+  ASSERT (close (dfd2) == 0);
+  ASSERT (unlink (BASE "sub1/file") == 0);
+  ASSERT (unlink (BASE "sub1/link") == 0);
+  ASSERT (unlink (BASE "sub2/link") == 0);
+  ASSERT (unlink (BASE "32") == 0);
+  ASSERT (unlink (BASE "33") == 0);
+  ASSERT (unlink (BASE "34") == 0);
+  ASSERT (rmdir (BASE "sub1") == 0);
+  ASSERT (rmdir (BASE "sub2") == 0);
+  ASSERT (unlink (BASE "link1") == 0);
+  ASSERT (unlink (BASE "link2") == 0);
+  ASSERT (unlink (BASE "link3") == 0);
+  ASSERT (unlink (BASE "link4") == 0);
+  ASSERT (unlink (BASE "link5") == 0);
+  ASSERT (unlink (BASE "file") == 0);
+  free (cwd);
+  return result;
+}
-- 
1.6.5.rc1


reply via email to

[Prev in Thread] Current Thread [Next in Thread]