Subject: bug#36502: Fwd: bug#36502: 27.0.50; infinite loop in file-name-case-insensitive-p

From: Paul Eggert <[email-masked]>
Date: Wed, 24 Jul 2019 14:36:25 -0700

Thanks for the patch. I improved it, wrote a NEWS entry and a test case for it, 
and installed the attached. I think this finishes off the issues in Bug#36502 
and so am closing the bug report.
>From a5063aa8b174db286a0e83b8ffdd4e65c521f733 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Wed, 24 Jul 2019 14:28:13 -0700
Subject: [PATCH] Do not treat ~nosuchuser as an absolute file name
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Derived from Ken Brown’s patch (Bug#36502#97).
* doc/lispref/files.texi (Relative File Names):
* etc/NEWS: Document this.
* src/fileio.c (user_homedir): New function.
(Fexpand_file_name, file_name_absolute_p): Use it.
(search_embedded_absfilename): Simplify via file_name_absolute_p.
* test/src/fileio-tests.el (fileio-tests--no-such-user): New test.
---
 doc/lispref/files.texi   |  10 +++-
 etc/NEWS                 |   3 +
 src/fileio.c             | 121 ++++++++++++++++++---------------------
 test/src/fileio-tests.el |  11 ++++
 4 files changed, 78 insertions(+), 67 deletions(-)

diff --git a/doc/lispref/files.texi b/doc/lispref/files.texi
index 0519f787dc..0ea8a4f0a1 100644
--- a/doc/lispref/files.texi
+++ b/doc/lispref/files.texi
@@ -2154,7 +2154,11 @@ Relative File Names
 
 @defun file-name-absolute-p filename
 This function returns @code{t} if file @var{filename} is an absolute
-file name or begins with @samp{~}, @code{nil} otherwise.
+file name, @code{nil} otherwise.  A file name is considered to be
+absolute if its first component is @samp{~}, or is @samp{~@var{user}}
+where @var{user} is a valid login name.  In the following examples,
+assume that there is a user named @samp{rms} but no user named
+@samp{nosuchuser}.
 
 @example
 @group
@@ -2162,6 +2166,10 @@ Relative File Names
      @result{} t
 @end group
 @group
+(file-name-absolute-p "~nosuchuser/foo")
+     @result{} nil
+@end group
+@group
 (file-name-absolute-p "rms/foo")
      @result{} nil
 @end group
diff --git a/etc/NEWS b/etc/NEWS
index 5313270411..08f0e654f7 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1815,6 +1815,9 @@ relative to the 'default-directory' of the current buffer.  We recommend
 always setting "$HOME" to an absolute file name, so that its meaning is
 independent of where Emacs was started.
 
+** file-name-absolute-p no longer considers "~foo" to be an absolute
+file name if there is no user named "foo".
+
 ** The FILENAME argument to 'file-name-base' is now mandatory and no
 longer defaults to 'buffer-file-name'.
 
diff --git a/src/fileio.c b/src/fileio.c
index e4269b96a3..d1a7f39ac9 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -744,6 +744,31 @@ file_name_absolute_no_tilde_p (Lisp_Object name)
   return IS_ABSOLUTE_FILE_NAME (SSDATA (name));
 }
 
+/* Return the home directory of the user NAME, or a null pointer if
+   NAME is empty or the user does not exist or the user's home
+   directory is not an absolute file name.  NAME is an array of bytes
+   that continues up to (but not including) the next NUL byte or
+   directory separator.  The returned string lives in storage good
+   until the next call to this or similar functions.  */
+static char *
+user_homedir (char const *name)
+{
+  ptrdiff_t length;
+  for (length = 0; name[length] && !IS_DIRECTORY_SEP (name[length]); length++)
+    continue;
+  if (length == 0)
+    return NULL;
+  USE_SAFE_ALLOCA;
+  char *p = SAFE_ALLOCA (length + 1);
+  memcpy (p, name, length);
+  p[length] = 0;
+  struct passwd *pw = getpwnam (p);
+  SAFE_FREE ();
+  if (!pw || (pw->pw_dir && !IS_ABSOLUTE_FILE_NAME (pw->pw_dir)))
+    return NULL;
+  return pw->pw_dir;
+}
+
 DEFUN ("expand-file-name", Fexpand_file_name, Sexpand_file_name, 1, 2, 0,
        doc: /* Convert filename NAME to absolute, and canonicalize it.
 Second arg DEFAULT-DIRECTORY is directory to start with if NAME is relative
@@ -788,7 +813,6 @@ the root directory.  */)
   char *target;
 
   ptrdiff_t tlen;
-  struct passwd *pw;
 #ifdef DOS_NT
   int drive = 0;
   bool collapse_newdir = true;
@@ -1153,39 +1177,29 @@ the root directory.  */)
 	}
       else			/* ~user/filename */
 	{
-	  char *o, *p;
-	  for (p = nm; *p && !IS_DIRECTORY_SEP (*p); p++)
-	    continue;
-	  o = SAFE_ALLOCA (p - nm + 1);
-	  memcpy (o, nm, p - nm);
-	  o[p - nm] = 0;
-
-	  block_input ();
-	  pw = getpwnam (o + 1);
-	  unblock_input ();
-	  if (pw)
+	  char *nmhome = user_homedir (nm + 1);
+	  if (nmhome)
 	    {
-	      Lisp_Object tem;
-
-	      newdir = pw->pw_dir;
-	      /* `getpwnam' may return a unibyte string, which will
-		 bite us when we expect the directory to be multibyte.  */
-	      tem = make_unibyte_string (newdir, strlen (newdir));
-	      newdirlim = newdir + SBYTES (tem);
-	      if (multibyte && !STRING_MULTIBYTE (tem))
+	      ptrdiff_t nmhomelen = strlen (nmhome);
+	      newdir = nmhome;
+	      newdirlim = newdir + nmhomelen;
+	      if (multibyte)
 		{
-		  hdir = DECODE_FILE (tem);
+		  AUTO_STRING_WITH_LEN (lisp_nmhome, nmhome, nmhomelen);
+		  hdir = DECODE_FILE (lisp_nmhome);
 		  newdir = SSDATA (hdir);
 		  newdirlim = newdir + SBYTES (hdir);
 		}
-	      nm = p;
+
+	      while (*++nm && !IS_DIRECTORY_SEP (*nm))
+		continue;
 #ifdef DOS_NT
 	      collapse_newdir = false;
 #endif
 	    }
 
 	  /* If we don't find a user of that name, leave the name
-	     unchanged; don't move nm forward to p.  */
+	     unchanged.  */
 	}
     }
 
@@ -1667,18 +1681,6 @@ See also the function `substitute-in-file-name'.")
 }
 #endif
 
-bool
-file_name_absolute_p (const char *filename)
-{
-  return
-    (IS_DIRECTORY_SEP (*filename) || *filename == '~'
-#ifdef DOS_NT
-     || (IS_DRIVE (*filename) && IS_DEVICE_SEP (filename[1])
-	 && IS_DIRECTORY_SEP (filename[2]))
-#endif
-     );
-}
-
 /* Put into BUF the concatenation of DIR and FILE, with an intervening
    directory separator if needed.  Return a pointer to the NUL byte
    at the end of the concatenated string.  */
@@ -1774,7 +1776,10 @@ get_homedir (void)
   return ahome;
 }
 
-/* If /~ or // appears, discard everything through first slash.  */
+/* If a directory separator followed by an absolute file name (e.g.,
+   "//foo", "/~", "/~someuser") appears in NM, return the address of
+   the absolute file name.  Otherwise return NULL.  ENDP is the
+   address of the null byte at the end of NM.  */
 static char *
 search_embedded_absfilename (char *nm, char *endp)
 {
@@ -1784,34 +1789,8 @@ search_embedded_absfilename (char *nm, char *endp)
 	&& !IS_DIRECTORY_SEP (p[1]));
 #endif
   for (; p < endp; p++)
-    {
-      if (IS_DIRECTORY_SEP (p[-1]) && file_name_absolute_p (p))
-	{
-	  char *s;
-	  for (s = p; *s && !IS_DIRECTORY_SEP (*s); s++)
-	    continue;
-	  if (p[0] == '~' && s > p + 1)	/* We've got "/~something/".  */
-	    {
-	      USE_SAFE_ALLOCA;
-	      char *o = SAFE_ALLOCA (s - p + 1);
-	      struct passwd *pw;
-	      memcpy (o, p, s - p);
-	      o [s - p] = 0;
-
-	      /* If we have ~user and `user' exists, discard
-		 everything up to ~.  But if `user' does not exist, leave
-		 ~user alone, it might be a literal file name.  */
-	      block_input ();
-	      pw = getpwnam (o + 1);
-	      unblock_input ();
-	      SAFE_FREE ();
-	      if (pw)
-		return p;
-	    }
-	  else
-	    return p;
-	}
-    }
+    if (IS_DIRECTORY_SEP (p[-1]) && file_name_absolute_p (p))
+      return p;
   return NULL;
 }
 
@@ -2696,13 +2675,23 @@ This happens for interactive use with M-x.  */)
 
 DEFUN ("file-name-absolute-p", Ffile_name_absolute_p, Sfile_name_absolute_p,
        1, 1, 0,
-       doc: /* Return t if FILENAME is an absolute file name or starts with `~'.
-On Unix, absolute file names start with `/'.  */)
+       doc: /* Return t if FILENAME is an absolute file name.
+On Unix, absolute file names start with `/'.  In Emacs, an absolute
+file name can also start with an initial `~' or `~USER' component,
+where USER is a valid login name.  */)
   (Lisp_Object filename)
 {
   CHECK_STRING (filename);
   return file_name_absolute_p (SSDATA (filename)) ? Qt : Qnil;
 }
+
+bool
+file_name_absolute_p (char const *filename)
+{
+  return (IS_ABSOLUTE_FILE_NAME (filename)
+	  || (filename[0] == '~'
+	      && (!filename[1] || user_homedir (&filename[1]))));
+}
 
 DEFUN ("file-exists-p", Ffile_exists_p, Sfile_exists_p, 1, 1, 0,
        doc: /* Return t if file FILENAME exists (whether or not you can read it).
diff --git a/test/src/fileio-tests.el b/test/src/fileio-tests.el
index 813ee5f798..09a5b147e1 100644
--- a/test/src/fileio-tests.el
+++ b/test/src/fileio-tests.el
@@ -136,3 +136,14 @@ fileio-tests--symlink-failure
          (name (expand-file-name "bar")))
     (should (and (file-name-absolute-p name)
                  (not (eq (aref name 0) ?~))))))
+
+(ert-deftest fileio-tests--no-such-user ()
+  "Test file-name-absolute-p on ~nosuchuser."
+  (unless (user-full-name "nosuchuser")
+    (should (not (file-name-absolute-p "~nosuchuser")))
+    (should (not (file-name-absolute-p "~nosuchuser/")))
+    (should (not (file-name-absolute-p "~nosuchuser//")))
+    (should (not (file-name-absolute-p "~nosuchuser/foo")))
+    (should (not (file-name-absolute-p "~nosuchuser/foo/")))
+    (should (not (file-name-absolute-p "~nosuchuser/foo//")))
+    (should (not (file-name-absolute-p "~nosuchuser/foo/bar")))))
-- 
2.17.1