Subject: 27.0.50; expand-file-name: default directory expanded twice if relative
From: Paul Eggert <[email-masked]>
To: Glenn Morris <[email-masked]>
Cc: [email-masked], immerrr again <[email-masked]>, Noam Postavsky <[email-masked]>
Date: Tue, 13 Nov 2018 10:26:43 -0800
List-Id: <debbugs-submit.debbugs.gnu.org>

> I think a non-absolute HOME is a user error.
> Eg cd $HOME may then not be idempotent.
That's true. However, POSIX says that sh treats ~/foo like $HOME/foo 
even when HOME is not absolute, and it's better if Emacs is consistent 
with POSIX as much as possible within the Emacs constraint that 
expand-file-name must expand to an absolute file name. So I implemented 
something along the line of Noam's suggestion by installing the attached 
patch into master; this should fix the bug originally reported.

Unlike Noam's suggestion, this patch causes Emacs to look at the current 
value of HOME, not the value HOME had when Emacs started up, as that 
corresponds more closely to POSIX sh.

From a2df04a05a68d5a4afa39cfe463f72108b31bc25 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Tue, 13 Nov 2018 09:29:14 -0800
Subject: [PATCH] Act like POSIX sh if $HOME is relative

POSIX says sh ~/foo should act like $HOME/foo even if $HOME is
relative, so be consistent with that (Bug#33255).
* admin/merge-gnulib (GNULIB_MODULES): Add dosname.
* src/buffer.c (init_buffer): Use emacs_wd to get
initial working directory with slash appended if needed.
(default-directory): Say it must be absolute.
* src/emacs.c (emacs_wd): New global variable.
(init_cmdargs): Dir arg is now char const *.
(main): Set emacs_wd.
* src/emacs.c (main) [NS_IMPL_COCOA]:
* src/fileio.c (Fexpand_file_name):
Use get_homedir instead of egetenv ("HOME").
* src/fileio.c: Include dosname.h, for IS_ABSOLUTE_FILE_NAME.
(splice_dir_file, get_homedir): New functions.
* src/xrdb.c (gethomedir): Remove.  All callers changed
to use get_homedir and splice_dir_file.
* test/src/fileio-tests.el (fileio-tests--relative-HOME): New test.
---
 admin/merge-gnulib       |  2 +-
 src/buffer.c             | 28 ++++++------------
 src/emacs.c              | 20 ++++++++-----
 src/fileio.c             | 62 +++++++++++++++++++++++++++++++++++++---
 src/lisp.h               |  3 ++
 src/xrdb.c               | 54 +++++++---------------------------
 test/src/fileio-tests.el |  8 ++++++
 7 files changed, 103 insertions(+), 74 deletions(-)

diff --git a/admin/merge-gnulib b/admin/merge-gnulib
index 575e3fa74a..84dcb0b875 100755
--- a/admin/merge-gnulib
+++ b/admin/merge-gnulib
@@ -30,7 +30,7 @@ GNULIB_MODULES=
   careadlinkat close-stream
   count-leading-zeros count-one-bits count-trailing-zeros
   crypto/md5-buffer crypto/sha1-buffer crypto/sha256-buffer crypto/sha512-buffer
-  d-type diffseq dtoastr dtotimespec dup2
+  d-type diffseq dosname dtoastr dtotimespec dup2
   environ execinfo explicit_bzero faccessat
   fcntl fcntl-h fdatasync fdopendir
   filemode filevercmp flexmember fpieee fstatat fsusage fsync
diff --git a/src/buffer.c b/src/buffer.c
index ac2de7d19f..90ef886b22 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -5268,9 +5268,7 @@ init_buffer_once (void)
 void
 init_buffer (int initialized)
 {
-  char *pwd;
   Lisp_Object temp;
-  ptrdiff_t len;
 
 #ifdef USE_MMAP_FOR_BUFFERS
   if (initialized)
@@ -5324,7 +5322,7 @@ init_buffer (int initialized)
   if (NILP (BVAR (&buffer_defaults, enable_multibyte_characters)))
     Fset_buffer_multibyte (Qnil);
 
-  pwd = emacs_get_current_dir_name ();
+  char const *pwd = emacs_wd;
 
   if (!pwd)
     {
@@ -5336,22 +5334,16 @@ init_buffer (int initialized)
     {
       /* Maybe this should really use some standard subroutine
          whose definition is filename syntax dependent.  */
-      len = strlen (pwd);
-      if (!(IS_DIRECTORY_SEP (pwd[len - 1])))
-        {
-          /* Grow buffer to add directory separator and '\0'.  */
-          pwd = realloc (pwd, len + 2);
-          if (!pwd)
-            fatal ("get_current_dir_name: %s\n", strerror (errno));
-          pwd[len] = DIRECTORY_SEP;
-          pwd[len + 1] = '\0';
-          len++;
-        }
+      ptrdiff_t len = strlen (pwd);
+      bool add_slash = ! IS_DIRECTORY_SEP (pwd[len - 1]);
 
       /* At this moment, we still don't know how to decode the directory
          name.  So, we keep the bytes in unibyte form so that file I/O
          routines correctly get the original bytes.  */
-      bset_directory (current_buffer, make_unibyte_string (pwd, len));
+      Lisp_Object dirname = make_unibyte_string (pwd, len + add_slash);
+      if (add_slash)
+	SSET (dirname, len, DIRECTORY_SEP);
+      bset_directory (current_buffer, dirname);
 
       /* Add /: to the front of the name
          if it would otherwise be treated as magic.  */
@@ -5372,8 +5364,6 @@ init_buffer (int initialized)
 
   temp = get_minibuffer (0);
   bset_directory (XBUFFER (temp), BVAR (current_buffer, directory));
-
-  free (pwd);
 }
 
 /* Similar to defvar_lisp but define a variable whose value is the
@@ -5706,8 +5696,8 @@ visual lines rather than logical lines.  See the documentation of
   DEFVAR_PER_BUFFER ("default-directory", &BVAR (current_buffer, directory),
 		     Qstringp,
 		     doc: /* Name of default directory of current buffer.
-It should be a directory name (as opposed to a directory file-name).
-On GNU and Unix systems, directory names end in a slash `/'.
+It should be an absolute directory name; on GNU and Unix systems,
+these names start with `/' or `~' and end with `/'.
 To interactively change the default directory, use command `cd'. */);
 
   DEFVAR_PER_BUFFER ("auto-fill-function", &BVAR (current_buffer, auto_fill_function),
diff --git a/src/emacs.c b/src/emacs.c
index 512174d562..acb4959bfe 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -204,6 +204,9 @@ HANDLE w32_daemon_event;
 char **initial_argv;
 int initial_argc;
 
+/* The name of the working directory, or NULL if this info is unavailable.  */
+char const *emacs_wd;
+
 static void sort_args (int argc, char **argv);
 static void syms_of_emacs (void);
 
@@ -406,7 +409,7 @@ terminate_due_to_signal (int sig, int backtrace_limit)
 /* Code for dealing with Lisp access to the Unix command line.  */
 
 static void
-init_cmdargs (int argc, char **argv, int skip_args, char *original_pwd)
+init_cmdargs (int argc, char **argv, int skip_args, char const *original_pwd)
 {
   int i;
   Lisp_Object name, dir, handler;
@@ -694,7 +697,7 @@ main (int argc, char **argv)
   char *ch_to_dir = 0;
 
   /* If we use --chdir, this records the original directory.  */
-  char *original_pwd = 0;
+  char const *original_pwd = 0;
 
   /* Record (approximately) where the stack begins.  */
   stack_bottom = (char *) &stack_bottom_variable;
@@ -794,6 +797,8 @@ main (int argc, char **argv)
       exit (0);
     }
 
+  emacs_wd = emacs_get_current_dir_name ();
+
   if (argmatch (argv, argc, "-chdir", "--chdir", 4, &ch_to_dir, &skip_args))
     {
 #ifdef WINDOWSNT
@@ -804,13 +809,14 @@ main (int argc, char **argv)
       filename_from_ansi (ch_to_dir, newdir);
       ch_to_dir = newdir;
 #endif
-      original_pwd = emacs_get_current_dir_name ();
       if (chdir (ch_to_dir) != 0)
         {
           fprintf (stderr, "%s: Can't chdir to %s: %s\n",
                    argv[0], ch_to_dir, strerror (errno));
           exit (1);
         }
+      original_pwd = emacs_wd;
+      emacs_wd = emacs_get_current_dir_name ();
     }
 
 #if defined (HAVE_SETRLIMIT) && defined (RLIMIT_STACK) && !defined (CYGWIN)
@@ -1289,21 +1295,21 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem
     {
 #ifdef NS_IMPL_COCOA
       /* Started from GUI? */
-      /* FIXME: Do the right thing if getenv returns NULL, or if
+      /* FIXME: Do the right thing if get_homedir returns "", or if
          chdir fails.  */
       if (! inhibit_window_system && ! isatty (STDIN_FILENO) && ! ch_to_dir)
-        chdir (getenv ("HOME"));
+        chdir (get_homedir ());
       if (skip_args < argc)
         {
           if (!strncmp (argv[skip_args], "-psn", 4))
             {
               skip_args += 1;
-              if (! ch_to_dir) chdir (getenv ("HOME"));
+              if (! ch_to_dir) chdir (get_homedir ());
             }
           else if (skip_args+1 < argc && !strncmp (argv[skip_args+1], "-psn", 4))
             {
               skip_args += 2;
-              if (! ch_to_dir) chdir (getenv ("HOME"));
+              if (! ch_to_dir) chdir (get_homedir ());
             }
         }
 #endif  /* COCOA */
diff --git a/src/fileio.c b/src/fileio.c
index 7fb865809f..e178c39fc1 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -96,6 +96,7 @@ along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.  */
 #include <acl.h>
 #include <allocator.h>
 #include <careadlinkat.h>
+#include <dosname.h>
 #include <fsusage.h>
 #include <stat-time.h>
 #include <tempname.h>
@@ -1093,8 +1094,7 @@ the root directory.  */)
 	{
 	  Lisp_Object tem;
 
-	  if (!(newdir = egetenv ("HOME")))
-	    newdir = newdirlim = "";
+	  newdir = get_homedir ();
 	  nm++;
 #ifdef WINDOWSNT
 	  if (newdir[0])
@@ -1109,7 +1109,7 @@ the root directory.  */)
 #endif
 	    tem = build_string (newdir);
 	  newdirlim = newdir + SBYTES (tem);
-	  /* `egetenv' may return a unibyte string, which will bite us
+	  /* get_homedir may return a unibyte string, which will bite us
 	     if we expect the directory to be multibyte.  */
 	  if (multibyte && !STRING_MULTIBYTE (tem))
 	    {
@@ -1637,7 +1637,6 @@ See also the function `substitute-in-file-name'.")
 }
 #endif
 
-/* If /~ or // appears, discard everything through first slash.  */
 static bool
 file_name_absolute_p (const char *filename)
 {
@@ -1650,6 +1649,61 @@ file_name_absolute_p (const char *filename)
      );
 }
 
+/* Put into BUF the concatenation of DIR and FILE, with an intervening
+   directory separator if needed.  Return a pointer to the null byte
+   at the end of the concatenated string.  */
+char *
+splice_dir_file (char *buf, char const *dir, char const *file)
+{
+  char *e = stpcpy (buf, dir);
+  *e = DIRECTORY_SEP;
+  e += ! (buf < e && IS_DIRECTORY_SEP (e[-1]));
+  return stpcpy (e, file);
+}
+
+/* Get the home directory, an absolute file name.  Return the empty
+   string on failure.  The returned value does not survive garbage
+   collection, calls to this function, or calls to the getpwnam class
+   of functions.  */
+char const *
+get_homedir (void)
+{
+  char const *home = egetenv ("HOME");
+  if (!home)
+    {
+      static char const *userenv[] = {"LOGNAME", "USER"};
+      struct passwd *pw = NULL;
+      for (int i = 0; i < ARRAYELTS (userenv); i++)
+	{
+	  char *user = egetenv (userenv[i]);
+	  if (user)
+	    {
+	      pw = getpwnam (user);
+	      if (pw)
+		break;
+	    }
+	}
+      if (!pw)
+	pw = getpwuid (getuid ());
+      if (pw)
+	home = pw->pw_dir;
+      if (!home)
+	return "";
+    }
+  if (IS_ABSOLUTE_FILE_NAME (home))
+    return home;
+  if (!emacs_wd)
+    error ("$HOME is relative to unknown directory");
+  static char *ahome;
+  static ptrdiff_t ahomesize;
+  ptrdiff_t ahomelenbound = strlen (emacs_wd) + 1 + strlen (home) + 1;
+  if (ahomesize <= ahomelenbound)
+    ahome = xpalloc (ahome, &ahomesize, ahomelenbound + 1 - ahomesize, -1, 1);
+  splice_dir_file (ahome, emacs_wd, home);
+  return ahome;
+}
+
+/* If /~ or // appears, discard everything through first slash.  */
 static char *
 search_embedded_absfilename (char *nm, char *endp)
 {
diff --git a/src/lisp.h b/src/lisp.h
index f8ffb33a64..7e7dba631f 100644
--- a/src/lisp.h
+++ b/src/lisp.h
@@ -4061,6 +4061,8 @@ extern void syms_of_marker (void);
 
 /* Defined in fileio.c.  */
 
+extern char *splice_dir_file (char *, char const *, char const *);
+extern char const *get_homedir (void);
 extern Lisp_Object expand_and_dir_to_file (Lisp_Object);
 extern Lisp_Object write_region (Lisp_Object, Lisp_Object, Lisp_Object,
 				 Lisp_Object, Lisp_Object, Lisp_Object,
@@ -4185,6 +4187,7 @@ extern void syms_of_frame (void);
 /* Defined in emacs.c.  */
 extern char **initial_argv;
 extern int initial_argc;
+extern char const *emacs_wd;
 #if defined (HAVE_X_WINDOWS) || defined (HAVE_NS)
 extern bool display_arg;
 #endif
diff --git a/src/xrdb.c b/src/xrdb.c
index 4abf1ad84e..87c2faf659 100644
--- a/src/xrdb.c
+++ b/src/xrdb.c
@@ -202,35 +202,6 @@ magic_db (const char *string, ptrdiff_t string_len, const char *class,
 }

-static char *
-gethomedir (void)
-{
-  struct passwd *pw;
-  char *ptr;
-  char *copy;
-
-  if ((ptr = getenv ("HOME")) == NULL)
-    {
-      if ((ptr = getenv ("LOGNAME")) != NULL
-	  || (ptr = getenv ("USER")) != NULL)
-	pw = getpwnam (ptr);
-      else
-	pw = getpwuid (getuid ());
-
-      if (pw)
-	ptr = pw->pw_dir;
-    }
-
-  if (ptr == NULL)
-    return xstrdup ("/");
-
-  ptrdiff_t len = strlen (ptr);
-  copy = xmalloc (len + 2);
-  strcpy (copy + len, "/");
-  return memcpy (copy, ptr, len);
-}
-
-
 /* Find the first element of SEARCH_PATH which exists and is readable,
    after expanding the %-escapes.  Return 0 if we didn't find any, and
    the path name of the one we found otherwise.  */
@@ -316,12 +287,11 @@ get_user_app (const char *class)
   if (! db)
     {
       /* Check in the home directory.  This is a bit of a hack; let's
-	 hope one's home directory doesn't contain any %-escapes.  */
-      char *home = gethomedir ();
+	 hope one's home directory doesn't contain ':' or '%'.  */
+      char const *home = get_homedir ();
       db = search_magic_path (home, class, "%L/%N");
       if (! db)
 	db = search_magic_path (home, class, "%N");
-      xfree (home);
     }
 
   return db;
@@ -346,10 +316,9 @@ get_user_db (Display *display)
   else
     {
       /* Use ~/.Xdefaults.  */
-      char *home = gethomedir ();
-      ptrdiff_t homelen = strlen (home);
-      char *filename = xrealloc (home, homelen + sizeof xdefaults);
-      strcpy (filename + homelen, xdefaults);
+      char const *home = get_homedir ();
+      char *filename = xmalloc (strlen (home) + 1 + sizeof xdefaults);
+      splice_dir_file (filename, home, xdefaults);
       db = XrmGetFileDatabase (filename);
       xfree (filename);
     }
@@ -380,13 +349,12 @@ get_environ_db (void)
       if (STRINGP (system_name))
 	{
 	  /* Use ~/.Xdefaults-HOSTNAME.  */
-	  char *home = gethomedir ();
-	  ptrdiff_t homelen = strlen (home);
-	  ptrdiff_t filenamesize = (homelen + sizeof xdefaults
-				    + 1 + SBYTES (system_name));
-	  p = filename = xrealloc (home, filenamesize);
-	  lispstpcpy (stpcpy (stpcpy (filename + homelen, xdefaults), "-"),
-		      system_name);
+	  char const *home = get_homedir ();
+	  p = filename = xmalloc (strlen (home) + 1 + sizeof xdefaults
+				  + 1 + SBYTES (system_name));
+	  char *e = splice_dir_file (p, home, xdefaults);
+	  *e++ = '/';
+	  lispstpcpy (e, system_name);
 	}
     }
 
diff --git a/test/src/fileio-tests.el b/test/src/fileio-tests.el
index 5d12685fa1..b7b78bbda0 100644
--- a/test/src/fileio-tests.el
+++ b/test/src/fileio-tests.el
@@ -95,3 +95,11 @@ fileio-tests--symlink-failure
   (should (equal (file-name-as-directory "d:/abc/") "d:/abc/"))
   (should (equal (file-name-as-directory "D:\\abc/") "d:/abc/"))
   (should (equal (file-name-as-directory "D:/abc//") "d:/abc//")))
+
+(ert-deftest fileio-tests--relative-HOME ()
+  "Test that expand-file-name works even when HOME is relative."
+  (let ((old-home (getenv "HOME")))
+    (setenv "HOME" "a/b/c")
+    (should (equal (expand-file-name "~/foo")
+                   (expand-file-name "a/b/c/foo")))
+    (setenv "HOME" old-home)))
-- 
2.19.1