From 84300b9dbb695dacef52c331920135f41c71c587 Mon Sep 17 00:00:00 2001
From: Kalle Olavi Niemitalo <kon@iki.fi>
Date: Sun, 13 Aug 2017 21:34:05 +0300
Subject: [PATCH] fsysopts, showtrans: Quote argz for sh when printing.

* utils/Makefile (SRCS): Add shquote.c.
(showtrans, fsysopts): Depend on shquote.o.
* utils/fsysopts.c (main): Quote each argument of the translator,
when printing them.
* utils/showtrans.c (main): Ditto.
* utils/shquote.c: New file.
* utils/shquote.h: New file.

Bug: https://savannah.gnu.org/bugs/?15806
---
 utils/Makefile    |   3 +-
 utils/fsysopts.c  |  21 ++-
 utils/showtrans.c |  20 ++-
 utils/shquote.c   | 387 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 utils/shquote.h   |  38 ++++++
 5 files changed, 458 insertions(+), 11 deletions(-)
 create mode 100644 utils/shquote.c
 create mode 100644 utils/shquote.h

diff --git a/utils/Makefile b/utils/Makefile
index bafafdc..b6e610a 100644
--- a/utils/Makefile
+++ b/utils/Makefile
@@ -33,7 +33,7 @@ SRCS = shd.c ps.c settrans.c syncfs.c showtrans.c addauth.c rmauth.c \
 	parse.c frobauth.c frobauth-mod.c setauth.c pids.c nonsugid.c \
 	unsu.c ftpcp.c ftpdir.c storeread.c storecat.c msgport.c \
 	rpctrace.c mount.c gcore.c fakeauth.c fakeroot.sh remap.sh \
-	nullauth.c match-options.c msgids.c rpcscan.c
+	nullauth.c match-options.c msgids.c rpcscan.c shquote.c
 
 OBJS = $(filter-out %.sh,$(SRCS:.c=.o))
 HURDLIBS = ps ihash store fshelp ports ftpconn shouldbeinlibc
@@ -57,6 +57,7 @@ addauth rmauth setauth unsu: frobauth.o
 rmauth setauth unsu: frobauth-mod.o
 ps w: psout.o ../libps/libps.a ../libihash/libihash.a
 portinfo: ../libihash/libihash.a ../libps/libps.a
+showtrans fsysopts: shquote.o
 
 storeinfo storecat storeread: ../libstore/libstore.a
 ftpcp ftpdir: ../libftpconn/libftpconn.a
diff --git a/utils/fsysopts.c b/utils/fsysopts.c
index 759ebe6..5086464 100644
--- a/utils/fsysopts.c
+++ b/utils/fsysopts.c
@@ -21,6 +21,7 @@
 /* Modified on 2016-09-13 by Kalle Olavi Niemitalo.  */
 
 #include <hurd.h>
+#include <locale.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -34,6 +35,8 @@
 
 #include <hurd/fsys.h>
 
+#include "shquote.h"
+
 const char *argp_program_version = STANDARD_HURD_VERSION (fsysopts);
 
 static struct argp_option options[] =
@@ -94,6 +97,9 @@ main(int argc, char *argv[])
 
   struct argp argp = {options, parse_opt, args_doc, doc};
 
+  /* Affects multibyte string validation in shquote_fput_argz.  */
+  setlocale (LC_ALL, "");
+
   argp_parse (&argp, argc, argv, ARGP_IN_ORDER, 0, 0);
 
   node = file_name_lookup (node_name, (deref ? 0 : O_NOLINK), 0666);
@@ -113,8 +119,10 @@ main(int argc, char *argv[])
       err = fsys_set_options (fsys, argz, argz_len, recursive);
       if (err)
 	{
-	  argz_stringify (argz, argz_len, ' ');
-	  error(5, err, "%s: %s", node_name, argz);
+	  char *s = shquote_stringify_argz (argz, argz_len);
+	  if (s == NULL)
+	    error (6, errno, "shquote_stringify_argz");
+	  error(5, err, "%s: %s", node_name, s);
 	}
     }
   else
@@ -122,8 +130,13 @@ main(int argc, char *argv[])
       err = file_get_fs_options (node, &argz, &argz_len);
       if (err)
 	error (5, err, "%s", node_name);
-      argz_stringify (argz, argz_len, ' ');
-      puts (argz);
+      if (argz_len != 0 && argz[argz_len - 1] != '\0')
+	error (5, 0, "%s: Arg vector is not null-terminated", node_name);
+      else
+	{
+	  shquote_fput_argz (argz, argz_len, stdout);
+	  putchar ('\n');
+	}
     }
 
   return 0;
diff --git a/utils/showtrans.c b/utils/showtrans.c
index e8e25a4..1255ba6 100644
--- a/utils/showtrans.c
+++ b/utils/showtrans.c
@@ -20,6 +20,7 @@
 /* Modified on 2016-09-13 by Kalle Olavi Niemitalo.  */
 
 #include <hurd.h>
+#include <locale.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -32,6 +33,8 @@
 #include <error.h>
 #include <argz.h>
 
+#include "shquote.h"
+
 const char *argp_program_version = STANDARD_HURD_VERSION (showtrans);
 
 static struct argp_option options[] =
@@ -75,15 +78,17 @@ main (int argc, char *argv[])
 	  switch (err)
 	    {
 	    case 0:
-	      /* Make the '\0's in TRANS printable.  */
-	      argz_stringify (trans, trans_len, ' ');
-
 	      if (!silent)
 		{
-		  if (print_prefix)
-		    printf ("%s: %.*s\n", name, (int) trans_len, trans);
+		  if (trans_len != 0 && trans[trans_len - 1] != '\0')
+		    error (0, 0, "%s: Arg vector is not null-terminated", name);
 		  else
-		    printf ("%.*s\n", (int) trans_len, trans);
+		    {
+		      if (print_prefix)
+			printf ("%s: ", name);
+		      shquote_fput_argz (trans, trans_len, stdout);
+		      putchar ('\n');
+		    }
 		}
 
 	      if (trans != buf)
@@ -140,6 +145,9 @@ main (int argc, char *argv[])
 
   struct argp argp = {options, parse_opt, args_doc, doc};
 
+  /* Affects multibyte string validation in shquote_fput_argz.  */
+  setlocale (LC_ALL, "");
+
   argp_parse (&argp, argc, argv, 0, 0, 0);
 
   return status;
diff --git a/utils/shquote.c b/utils/shquote.c
new file mode 100644
index 0000000..4445cc3
--- /dev/null
+++ b/utils/shquote.c
@@ -0,0 +1,387 @@
+/* Quote strings for the shell.
+
+   Copyright (C) 2017 Kalle Olavi Niemitalo.
+
+   Permission to use, copy, modify, and/or distribute this software for any
+   purpose with or without fee is hereby granted, provided that the above
+   copyright notice and this permission notice appear in all copies.
+
+   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.  */
+
+#include <argz.h>
+#include <assert.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <wchar.h>
+#include <wctype.h>
+#include "shquote.h"
+
+static bool
+is_safe_char (unsigned char c)
+{
+  switch (c)
+    {
+    case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G':
+    case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N':
+    case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U':
+    case 'V': case 'W': case 'X': case 'Y': case 'Z':
+    case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g':
+    case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n':
+    case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u':
+    case 'v': case 'w': case 'x': case 'y': case 'z':
+    case '0': case '1': case '2': case '3': case '4': case '5': case '6':
+    case '7': case '8': case '9':
+    case '.': case '_': case '-':
+      /* POSIX portable file name character */
+      return true;
+
+    case '/': case '=': case ',': case '+': case ':':
+      /* Seems safe otherwise */
+      return true;
+
+    default:
+      return false;
+    }
+}
+
+static bool
+is_safe_str (const char *p, const char *const end)
+{
+  for (; p != end; ++p)
+    {
+      if (!is_safe_char (*(const unsigned char *) p))
+	return false;
+    }
+
+  return true;
+}
+
+static int
+backslashify_char_unlocked (unsigned char c, FILE *stream)
+{
+  switch (c)
+    {
+    case '\\': return fputs_unlocked ("\\\\", stream);
+    case '\"': return fputs_unlocked ("\\\"", stream);
+    case '\'': return fputs_unlocked ("\\'", stream);
+    case '\a': return fputs_unlocked ("\\a", stream);
+    case '\b': return fputs_unlocked ("\\b", stream);
+    case '\f': return fputs_unlocked ("\\f", stream);
+    case '\n': return fputs_unlocked ("\\n", stream);
+    case '\r': return fputs_unlocked ("\\r", stream);
+    case '\t': return fputs_unlocked ("\\t", stream);
+    case '\v': return fputs_unlocked ("\\v", stream);
+
+    default: return fprintf (stream, "\\x%.2X", c);
+    }
+}
+
+static int
+backslashify_str_unlocked (const char *s, const char *const end, FILE *stream)
+{
+  for (; s != end; s++)
+    {
+      unsigned char c = *(const unsigned char *) s;
+      if (backslashify_char_unlocked (c, stream) == EOF)
+	return EOF;
+    }
+
+  return 0;
+}
+
+enum quoting_style
+{
+  SINGLE_QUOTE,
+  DOUBLE_QUOTE,
+  DOLLAR_SINGLE_QUOTE
+};
+
+static enum quoting_style
+choose_quoting_style (const char *s, const char *const end)
+{
+  mbstate_t mbstate;
+  memset (&mbstate, 0, sizeof mbstate);
+
+  const char *p = s;
+  while (p != end)
+    {
+      const char *seg_end = strpbrk (p, "'\\");
+      if (seg_end == NULL)
+	seg_end = end;
+
+      while (p != seg_end)
+	{
+	  wchar_t wc;
+	  size_t len = mbrtowc (&wc, p, seg_end - p, &mbstate);
+	  switch (len)
+	    {
+	    case (size_t) -2:	/* incomplete multibyte char */
+	    case (size_t) -1:	/* errno == EILSEQ */
+	    case 0:		/* wc == L'\0' */
+	      return DOLLAR_SINGLE_QUOTE;
+
+	    default:
+	      if (iswcntrl (wc))
+		return DOLLAR_SINGLE_QUOTE;
+	      p += len;
+	      break;
+	    }
+	}
+
+      if (!mbsinit (&mbstate))
+	return DOLLAR_SINGLE_QUOTE;
+
+      if (p != end)
+	{
+	  p++;
+	}
+    }
+
+  if (memchr (s, '\'', end - s) == NULL)
+    return SINGLE_QUOTE;
+  else
+    return DOUBLE_QUOTE;
+}
+
+static int
+dollar_single_quote_unlocked (const char *s, const char *const end, FILE *stream)
+{
+  if (fputs_unlocked ("$'", stream) == EOF)
+    return EOF;
+
+  mbstate_t mbstate;
+  memset (&mbstate, 0, sizeof mbstate);
+
+  while (s != end)
+    {
+      const char *seg_end = strpbrk (s, "'\\");
+      if (seg_end == NULL)
+	seg_end = end;
+
+      while (s != seg_end)
+	{
+	  wchar_t wc;
+	  size_t len = mbrtowc (&wc, s, seg_end - s, &mbstate);
+	  switch (len)
+	    {
+	    case (size_t) -1:	/* errno == EILSEQ */
+	    case 0:		/* wc == L'\0' */
+	      if (backslashify_char_unlocked (*(const unsigned char *) s,
+					      stream) == EOF)
+		return EOF;
+	      s++;
+	      memset (&mbstate, 0, sizeof mbstate);
+	      break;
+
+	    case (size_t) -2:	/* incomplete multibyte char */
+	      if (backslashify_str_unlocked (s, seg_end, stream) == EOF)
+		return EOF;
+	      s = seg_end;
+	      memset (&mbstate, 0, sizeof mbstate);
+	      break;
+
+	    default:
+	      if (iswcntrl (wc))
+		{
+		  if (backslashify_str_unlocked (s, s + len, stream) == EOF)
+		    return EOF;
+		}
+	      else
+		{
+		  if (fwrite_unlocked (s, 1, len, stream) != len)
+		    return EOF;
+		}
+	      s += len;
+	      break;
+	    }
+	}
+
+      if (s != end)
+	{
+	  if (backslashify_char_unlocked (*(const unsigned char *) s,
+					  stream) == EOF)
+	    return EOF;
+	  s++;
+	}
+    }
+
+  if (putc_unlocked ('\'', stream) == EOF)
+    return EOF;
+
+  return 0;
+}
+
+static int
+single_quote_unlocked (const char *s, const char *end, FILE *stream)
+{
+  if (putc_unlocked ('\'', stream) == EOF)
+    return EOF;
+
+  /* shquote_fputs_unlocked does not call single_quote_unlocked if the
+     string contains any single-quote characters. */
+  assert (memchr (s, '\'', end - s) == NULL);
+
+  if (fwrite_unlocked (s, 1, end - s, stream) != end - s)
+    return EOF;
+
+  if (putc_unlocked ('\'', stream) == EOF)
+    return EOF;
+
+  return 0;
+}
+
+static int
+double_quote_unlocked (const char *s, const char *const end, FILE *stream)
+{
+  if (putc_unlocked ('"', stream) == EOF)
+    return EOF;
+
+  for (; s != end; s++)
+    {
+      unsigned char c = *(const unsigned char *) s;
+      switch (c)
+	{
+	case '\\':
+	case '$':
+	case '`':
+	case '"':
+	  if (putc_unlocked ('\\', stream) == EOF)
+	    return EOF;
+	  /* fall through */
+	default:
+	  if (putc_unlocked (c, stream) == EOF)
+	    return EOF;
+	  break;
+	}
+    }
+
+  if (putc_unlocked ('"', stream) == EOF)
+    return EOF;
+
+  return 0;
+}
+
+/* Same as shquote_fputs documented in shquote.h, except uses unlocked
+   FILE functions.  */
+static int
+shquote_fputs_unlocked (const char *s, FILE *stream)
+{
+  const char *const end = strchr (s, '\0');
+  const char *eq = NULL;
+
+  if (s == end)
+    {
+      /* Special case for empty string.  */
+      if (fputs_unlocked ("''", stream) == EOF)
+	return EOF;
+      else
+	return 0;
+    }
+
+  eq = memchr (s, '=', end - s);
+  if (eq != NULL && eq != s && is_safe_str (s, eq))
+    {
+      /* The string starts with one or more safe characters followed
+	 by an equals sign.  Write that part without quoting, and
+	 quote the rest separately.  Switches like
+	 --long-option="value with spaces" look better that way.  Do
+	 not require dashes at the start.  */
+      size_t len = eq + 1 - s;
+      if (fwrite_unlocked (s, 1, len, stream) != len)
+	return EOF;
+      s = eq + 1;
+    }
+
+  if (is_safe_str (s, end))
+    {
+      /* No quoting needed.  */
+      if (fputs_unlocked (s, stream) == EOF)
+	return EOF;
+      else
+	return 0;
+    }
+
+  /* We will quote the string, but how?  */
+  switch (choose_quoting_style (s, end))
+    {
+    case SINGLE_QUOTE:
+      return single_quote_unlocked (s, end, stream);
+    case DOUBLE_QUOTE:
+      return double_quote_unlocked (s, end, stream);
+    default:
+      assert (0);
+      /* fall through */
+    case DOLLAR_SINGLE_QUOTE:
+      return dollar_single_quote_unlocked (s, end, stream);
+    }
+}
+
+int
+shquote_fputs (const char *s, FILE *stream)
+{
+  int ret;
+
+  flockfile (stream);
+  ret = shquote_fputs_unlocked (s, stream);
+  funlockfile (stream);
+
+  return ret;
+}
+
+/* Write the strings of the '\0' separated arg vector ARGZ to STREAM,
+   quoting metacharacters for sh.  */
+int
+shquote_fput_argz (const char *argz, size_t len, FILE *stream)
+{
+  const char *s = argz_next (argz, len, NULL);
+  if (s != NULL)
+    {
+      if (shquote_fputs (s, stream) == EOF)
+	return EOF;
+      while ((s = argz_next (argz, len, s)) != NULL)
+	{
+	  if (fputc (' ', stream) == EOF
+	      || shquote_fputs (s, stream) == EOF)
+	    return EOF;
+	}
+    }
+  return 0;
+}
+
+char *
+shquote_stringify_argz (const char *argz, size_t len)
+{
+  char *retbuf = NULL;
+  size_t retsize = 0;
+  FILE *stream;
+  error_t err = 0;
+
+  stream = open_memstream (&retbuf, &retsize);
+  if (stream == NULL)
+    return NULL;
+
+  if (shquote_fput_argz (argz, len, stream) == EOF)
+    {
+      err = errno;
+      fclose (stream);
+      free (retbuf);
+      errno = err;
+      return NULL;
+    }
+
+  if (fclose (stream) == EOF)
+    {
+      err = errno;
+      free (retbuf);
+      errno = err;
+      return NULL;
+    }
+
+  return retbuf;
+}
diff --git a/utils/shquote.h b/utils/shquote.h
new file mode 100644
index 0000000..1e6392b
--- /dev/null
+++ b/utils/shquote.h
@@ -0,0 +1,38 @@
+/* Quote strings for the shell.
+
+   Copyright (C) 2017 Kalle Olavi Niemitalo.
+
+   Permission to use, copy, modify, and/or distribute this software for any
+   purpose with or without fee is hereby granted, provided that the above
+   copyright notice and this permission notice appear in all copies.
+
+   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.  */
+
+#ifndef _HURD_SHQUOTE_H_
+#define _HURD_SHQUOTE_H_
+
+#include <stdio.h>
+
+/* Write the string S to STREAM, quoting metacharacters for the shell.
+   If S is empty, write only a pair of quotes.  On success, return a
+   nonnegative integer.  On error, set errno and return EOF.  */
+int shquote_fputs (const char *s, FILE *stream);
+
+/* Write the strings of the '\0' separated arg vector ARGZ to STREAM,
+   quoting metacharacters for the shell.  Delimit args with spaces.
+   On success, return a nonnegative integer.  On error, set errno and
+   return EOF.  */
+int shquote_fput_argz (const char *argz, size_t len, FILE *stream);
+
+/* Format the strings of the '\0' separated arg vector ARGZ to a
+   malloced string, quoting metacharacters for the shell.  Delimit
+   args with spaces.  On error, set errno and return NULL.  */
+char *shquote_stringify_argz (const char *argz, size_t len);
+
+#endif	/* _HURD_SHQUOTE_H */
-- 
2.1.4

