readlink: fix Solaris 9 bug with trailing slash
authorEric Blake <ebb9@byu.net>
Tue, 22 Sep 2009 23:15:04 +0000 (17:15 -0600)
committerEric Blake <ebb9@byu.net>
Wed, 23 Sep 2009 11:39:19 +0000 (05:39 -0600)
readlink("link/",buf,len) mistakenly succeeded.

* lib/readlink.c (rpl_readlink): Work around trailing slash bug.
* m4/readlink.m4 (gl_FUNC_READLINK): Detect the bug.
* doc/posix-functions/readlink.texi (readlink): Document this.
* modules/readlink-tests: New test.
* tests/test-readlink.c: Likewise.

Signed-off-by: Eric Blake <ebb9@byu.net>
ChangeLog
doc/posix-functions/readlink.texi
lib/readlink.c
m4/readlink.m4
modules/readlink-tests [new file with mode: 0644]
tests/test-readlink.c [new file with mode: 0644]

index 2a0d7f7..9337f56 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,12 @@
 2009-09-23  Eric Blake  <ebb9@byu.net>
 
+       readlink: fix Solaris 9 bug with trailing slash
+       * lib/readlink.c (rpl_readlink): Work around trailing slash bug.
+       * m4/readlink.m4 (gl_FUNC_READLINK): Detect the bug.
+       * doc/posix-functions/readlink.texi (readlink): Document this.
+       * modules/readlink-tests: New test.
+       * tests/test-readlink.c: Likewise.
+
        readlink: fix cygwin 1.5.x bug with return type
        * m4/readlink.m4 (gl_FUNC_READLINK): Require correct signature.
        * lib/unistd.in.h (readlink): Use ssize_t.
index dd7bcb9..959f62d 100644 (file)
@@ -9,6 +9,9 @@ Gnulib module: readlink
 Portability problems fixed by Gnulib:
 @itemize
 @item
+Some platforms mistakenly succeed on @code{readlink("link/",buf,len)}:
+Solaris 9.
+@item
 On some platforms, @code{readlink} returns @code{int} instead of
 @code{ssize_t}:
 FreeBSD 6.0, OpenBSD 3.8, Cygwin 1.5.x.
index 58379b1..0c6f09a 100644 (file)
@@ -47,11 +47,27 @@ readlink (const char *name, char *buf _UNUSED_PARAMETER_,
 # undef readlink
 
 /* readlink() wrapper that uses correct types, for systems like cygwin
-   1.5.x where readlink returns int.  */
+   1.5.x where readlink returns int, and which rejects trailing slash,
+   for Solaris 9.  */
 
 ssize_t
 rpl_readlink (const char *name, char *buf, size_t bufsize)
 {
+# if READLINK_TRAILING_SLASH_BUG
+  size_t len = strlen (name);
+  if (len && name[len - 1] == '/')
+    {
+      /* Even if name without the slash is a symlink to a directory,
+         both lstat() and stat() must resolve the trailing slash to
+         the directory rather than the symlink.  We can therefore
+         safely use stat() to distinguish between EINVAL and
+         ENOTDIR/ENOENT, avoiding extra overhead of rpl_lstat().  */
+      struct stat st;
+      if (stat (name, &st) == 0)
+        errno = EINVAL;
+      return -1;
+    }
+# endif /* READLINK_TRAILING_SLASH_BUG */
   return readlink (name, buf, bufsize);
 }
 
index b7b74de..fa7c2dd 100644 (file)
@@ -1,4 +1,4 @@
-# readlink.m4 serial 6
+# readlink.m4 serial 7
 dnl Copyright (C) 2003, 2007, 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,
@@ -21,7 +21,24 @@ AC_DEFUN([gl_FUNC_READLINK],
       /* Cause compilation failure if original declaration has wrong type.  */
       ssize_t readlink (const char *, char *, size_t);]])],
          [gl_cv_decl_readlink_works=yes], [gl_cv_decl_readlink_works=no])])
-    if test "$gl_cv_decl_readlink_works" != yes; then
+    AC_CACHE_CHECK([whether readlink handles trailing slash correctly],
+      [gl_cv_func_readlink_works],
+      [# We have readlink, so assume ln -s works.
+       ln -s conftest.no-such conftest.link
+       AC_RUN_IFELSE(
+         [AC_LANG_PROGRAM(
+           [[#include <unistd.h>
+]], [[char buf[20];
+      return readlink ("conftest.link/", buf, sizeof buf) != -1;]])],
+         [gl_cv_func_readlink_works=yes], [gl_cv_func_readlink_works=no],
+         [gl_cv_func_readlink_works="guessing no"])
+      rm -f conftest.link])
+    if test "$gl_cv_func_readlink_works" != yes; then
+      AC_DEFINE([READLINK_TRAILING_SLASH_BUG], [1], [Define to 1 if readlink
+        fails to recognize a trailing slash.])
+      REPLACE_READLINK=1
+      AC_LIBOBJ([readlink])
+    elif test "$gl_cv_decl_readlink_works" != yes; then
       REPLACE_READLINK=1
       AC_LIBOBJ([readlink])
     fi
diff --git a/modules/readlink-tests b/modules/readlink-tests
new file mode 100644 (file)
index 0000000..84994d5
--- /dev/null
@@ -0,0 +1,11 @@
+Files:
+tests/test-readlink.c
+
+Depends-on:
+symlink
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-readlink
+check_PROGRAMS += test-readlink
diff --git a/tests/test-readlink.c b/tests/test-readlink.c
new file mode 100644 (file)
index 0000000..f0f921e
--- /dev/null
@@ -0,0 +1,118 @@
+/* Tests of readlink.
+   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 <ebb9@byu.net>, 2009.  */
+
+#include <config.h>
+
+#include <unistd.h>
+
+#include <fcntl.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#define ASSERT(expr) \
+  do                                                                         \
+    {                                                                        \
+      if (!(expr))                                                           \
+       {                                                                    \
+         fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__);  \
+         fflush (stderr);                                                   \
+         abort ();                                                          \
+       }                                                                    \
+    }                                                                        \
+  while (0)
+
+#define BASE "test-readlink.t"
+
+int
+main ()
+{
+  char buf[80];
+
+  /* Remove any leftovers from a previous partial run.  */
+  ASSERT (system ("rm -rf " BASE "*") == 0);
+
+  /* Sanity checks of failures.  Mingw lacks symlink, but readlink can
+     still distinguish between various errors.  */
+  memset (buf, 0xff, sizeof buf);
+  errno = 0;
+  ASSERT (readlink ("no_such", buf, sizeof buf) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (readlink ("no_such/", buf, sizeof buf) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (readlink ("", buf, sizeof buf) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (readlink (".", buf, sizeof buf) == -1);
+  ASSERT (errno == EINVAL);
+  errno = 0;
+  ASSERT (readlink ("./", buf, sizeof buf) == -1);
+  ASSERT (errno == EINVAL);
+  ASSERT (close (creat (BASE "file", 0600)) == 0);
+  errno = 0;
+  ASSERT (readlink (BASE "file", buf, sizeof buf) == -1);
+  ASSERT (errno == EINVAL);
+  errno = 0;
+  ASSERT (readlink (BASE "file/", buf, sizeof buf) == -1);
+  ASSERT (errno == ENOTDIR);
+  ASSERT (unlink (BASE "file") == 0);
+
+  /* Now test actual symlinks.  */
+  if (symlink (BASE "dir", BASE "link"))
+    {
+      fputs ("skipping test: symlinks not supported on this filesystem\n",
+            stderr);
+      return 77;
+    }
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+  errno = 0;
+  ASSERT (readlink (BASE "link/", buf, sizeof buf) == -1);
+  ASSERT (errno == EINVAL);
+  {
+    /* Up till now, no readlink has been successful, so buf should be
+       unchanged.  */
+    int i;
+    for (i = 0; i < sizeof buf; i++)
+      ASSERT (buf[i] == (char) 0xff);
+  }
+  {
+    size_t len = strlen (BASE "dir");
+    /* When passing too small of a buffer, expect the truncated
+       length.  However, a size of 0 is not portable enough to
+       test.  */
+    ASSERT (readlink (BASE "link", buf, 1) == 1);
+    ASSERT (buf[0] == BASE[0]);
+    ASSERT (buf[1] == (char) 0xff);
+    ASSERT (readlink (BASE "link", buf, len) == len);
+    ASSERT (strncmp (buf, BASE "dir", len) == 0);
+    ASSERT (buf[len] == (char) 0xff);
+    ASSERT (readlink (BASE "link", buf, sizeof buf) == len);
+    ASSERT (strncmp (buf, BASE "dir", len) == 0);
+    /* POSIX says rest of buf is unspecified; but in practice, it is
+       either left alone, or NUL-terminated.  */
+    ASSERT (buf[len] == '\0' || buf[len] == (char) 0xff);
+  }
+  ASSERT (rmdir (BASE "dir") == 0);
+  ASSERT (unlink (BASE "link") == 0);
+
+  return 0;
+}