b1dca6
commit 92954ffa5a5662fbfde14febd7e5dcc358c85470
b1dca6
Author: Carlos O'Donell <carlos@redhat.com>
b1dca6
Date:   Wed Jan 8 13:24:42 2020 -0500
b1dca6
b1dca6
    localedef: Add verbose messages for failure paths.
b1dca6
    
b1dca6
    During testing of localedef running in a minimal container
b1dca6
    there were several error cases which were hard to diagnose
b1dca6
    since they appeared as strerror (errno) values printed by the
b1dca6
    higher level functions.  This change adds three new verbose
b1dca6
    messages for potential failure paths.  The new messages give
b1dca6
    the user the opportunity to use -v and display additional
b1dca6
    information about why localedef might be failing.  I found
b1dca6
    these messages useful myself while writing a localedef
b1dca6
    container test for --no-hard-links.
b1dca6
    
b1dca6
    Since the changes cleanup the code that handle codeset
b1dca6
    normalization we add tst-localedef-path-norm which contains
b1dca6
    many sub-tests to verify the correct expected normalization of
b1dca6
    codeset strings both when installing to default paths (the
b1dca6
    only time normalization is enabled) and installing to absolute
b1dca6
    paths.  During the refactoring I created at least one
b1dca6
    buffer-overflow which valgrind caught, but these tests did not
b1dca6
    catch because the exec in the container had a very clean heap
b1dca6
    with zero-initialized memory. However, between valgrind and
b1dca6
    the tests the results are clean.
b1dca6
    
b1dca6
    The new tst-localedef-path-norm passes without regression on
b1dca6
    x86_64.
b1dca6
    
b1dca6
    Change-Id: I28b9f680711ff00252a2cb15625b774cc58ecb9d
b1dca6
b1dca6
diff --git a/include/programs/xasprintf.h b/include/programs/xasprintf.h
b1dca6
new file mode 100644
b1dca6
index 0000000000000000..53193ba3837f7418
b1dca6
--- /dev/null
b1dca6
+++ b/include/programs/xasprintf.h
b1dca6
@@ -0,0 +1,24 @@
b1dca6
+/* asprintf with out of memory checking
b1dca6
+   Copyright (C) 2019 Free Software Foundation, Inc.
b1dca6
+   This file is part of the GNU C Library.
b1dca6
+
b1dca6
+   This program is free software; you can redistribute it and/or modify
b1dca6
+   it under the terms of the GNU General Public License as published
b1dca6
+   by the Free Software Foundation; version 2 of the License, or
b1dca6
+   (at your option) any later version.
b1dca6
+
b1dca6
+   This program is distributed in the hope that it will be useful,
b1dca6
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
b1dca6
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
b1dca6
+   GNU General Public License for more details.
b1dca6
+
b1dca6
+   You should have received a copy of the GNU General Public License
b1dca6
+   along with this program; if not, see <https://www.gnu.org/licenses/>.  */
b1dca6
+
b1dca6
+#ifndef _XASPRINTF_H
b1dca6
+#define _XASPRINTF_H	1
b1dca6
+
b1dca6
+extern char *xasprintf (const char *format, ...)
b1dca6
+    __attribute__ ((__format__ (__printf__, 1, 2), __warn_unused_result__));
b1dca6
+
b1dca6
+#endif /* xasprintf.h */
b1dca6
diff --git a/locale/Makefile b/locale/Makefile
b1dca6
index 23a71321b6646c49..4278350cdc7be28d 100644
b1dca6
--- a/locale/Makefile
b1dca6
+++ b/locale/Makefile
b1dca6
@@ -28,6 +28,7 @@ routines	= setlocale findlocale loadlocale loadarchive \
b1dca6
 		  localeconv nl_langinfo nl_langinfo_l mb_cur_max \
b1dca6
 		  newlocale duplocale freelocale uselocale
b1dca6
 tests		= tst-C-locale tst-locname tst-duplocale
b1dca6
+tests-container	= tst-localedef-path-norm
b1dca6
 categories	= ctype messages monetary numeric time paper name \
b1dca6
 		  address telephone measurement identification collate
b1dca6
 aux		= $(categories:%=lc-%) $(categories:%=C-%) SYS_libc C_name \
b1dca6
@@ -54,7 +55,7 @@ localedef-modules	:= localedef $(categories:%=ld-%) \
b1dca6
 localedef-aux		:= md5
b1dca6
 locale-modules		:= locale locale-spec
b1dca6
 lib-modules		:= charmap-dir simple-hash xmalloc xstrdup \
b1dca6
-			   record-status
b1dca6
+			   record-status xasprintf
b1dca6
 
b1dca6
 
b1dca6
 GPERF = gperf
b1dca6
diff --git a/locale/programs/localedef.c b/locale/programs/localedef.c
b1dca6
index d718d2e9f47382bc..9a57d2cb435b25ed 100644
b1dca6
--- a/locale/programs/localedef.c
b1dca6
+++ b/locale/programs/localedef.c
b1dca6
@@ -174,14 +174,14 @@ static struct argp argp =
b1dca6
 
b1dca6
 /* Prototypes for local functions.  */
b1dca6
 static void error_print (void);
b1dca6
-static const char *construct_output_path (char *path);
b1dca6
-static const char *normalize_codeset (const char *codeset, size_t name_len);
b1dca6
+static char *construct_output_path (char *path);
b1dca6
+static char *normalize_codeset (const char *codeset, size_t name_len);
b1dca6
 
b1dca6
 
b1dca6
 int
b1dca6
 main (int argc, char *argv[])
b1dca6
 {
b1dca6
-  const char *output_path;
b1dca6
+  char *output_path;
b1dca6
   int cannot_write_why;
b1dca6
   struct charmap_t *charmap;
b1dca6
   struct localedef_t global;
b1dca6
@@ -226,7 +226,8 @@ main (int argc, char *argv[])
b1dca6
     }
b1dca6
 
b1dca6
   /* The parameter describes the output path of the constructed files.
b1dca6
-     If the described files cannot be written return a NULL pointer.  */
b1dca6
+     If the described files cannot be written return a NULL pointer.
b1dca6
+     We don't free output_path because we will exit.  */
b1dca6
   output_path  = construct_output_path (argv[remaining]);
b1dca6
   if (output_path == NULL && ! no_archive)
b1dca6
     error (4, errno, _("cannot create directory for output files"));
b1dca6
@@ -424,20 +425,16 @@ more_help (int key, const char *text, void *input)
b1dca6
     {
b1dca6
     case ARGP_KEY_HELP_EXTRA:
b1dca6
       /* We print some extra information.  */
b1dca6
-      if (asprintf (&tp, gettext ("\
b1dca6
+      tp = xasprintf (gettext ("\
b1dca6
 For bug reporting instructions, please see:\n\
b1dca6
-%s.\n"), REPORT_BUGS_TO) < 0)
b1dca6
-	return NULL;
b1dca6
-      if (asprintf (&cp, gettext ("\
b1dca6
+%s.\n"), REPORT_BUGS_TO);
b1dca6
+      cp = xasprintf (gettext ("\
b1dca6
 System's directory for character maps : %s\n\
b1dca6
 		       repertoire maps: %s\n\
b1dca6
 		       locale path    : %s\n\
b1dca6
 %s"),
b1dca6
-		    CHARMAP_PATH, REPERTOIREMAP_PATH, LOCALE_PATH, tp) < 0)
b1dca6
-	{
b1dca6
-	  free (tp);
b1dca6
-	  return NULL;
b1dca6
-	}
b1dca6
+		    CHARMAP_PATH, REPERTOIREMAP_PATH, LOCALE_PATH, tp);
b1dca6
+      free (tp);
b1dca6
       return cp;
b1dca6
     default:
b1dca6
       break;
b1dca6
@@ -467,15 +464,13 @@ error_print (void)
b1dca6
 }
b1dca6
 
b1dca6
 
b1dca6
-/* The parameter to localedef describes the output path.  If it does
b1dca6
-   contain a '/' character it is a relative path.  Otherwise it names the
b1dca6
-   locale this definition is for.  */
b1dca6
-static const char *
b1dca6
+/* The parameter to localedef describes the output path.  If it does contain a
b1dca6
+   '/' character it is a relative path.  Otherwise it names the locale this
b1dca6
+   definition is for.   The returned path must be freed by the caller. */
b1dca6
+static char *
b1dca6
 construct_output_path (char *path)
b1dca6
 {
b1dca6
-  const char *normal = NULL;
b1dca6
   char *result;
b1dca6
-  char *endp;
b1dca6
 
b1dca6
   if (strchr (path, '/') == NULL)
b1dca6
     {
b1dca6
@@ -483,50 +478,44 @@ construct_output_path (char *path)
b1dca6
 	 contains a reference to the codeset.  This should be
b1dca6
 	 normalized.  */
b1dca6
       char *startp;
b1dca6
+      char *endp = NULL;
b1dca6
+      char *normal = NULL;
b1dca6
 
b1dca6
       startp = path;
b1dca6
-      /* We must be prepared for finding a CEN name or a location of
b1dca6
-	 the introducing `.' where it is not possible anymore.  */
b1dca6
+      /* Either we have a '@' which starts a CEN name or '.' which starts the
b1dca6
+	 codeset specification.  The CEN name starts with '@' and may also have
b1dca6
+	 a codeset specification, but we do not normalize the string after '@'.
b1dca6
+	 If we only find the codeset specification then we normalize only the codeset
b1dca6
+	 specification (but not anything after a subsequent '@').  */
b1dca6
       while (*startp != '\0' && *startp != '@' && *startp != '.')
b1dca6
 	++startp;
b1dca6
       if (*startp == '.')
b1dca6
 	{
b1dca6
 	  /* We found a codeset specification.  Now find the end.  */
b1dca6
 	  endp = ++startp;
b1dca6
+
b1dca6
+	  /* Stop at the first '@', and don't normalize anything past that.  */
b1dca6
 	  while (*endp != '\0' && *endp != '@')
b1dca6
 	    ++endp;
b1dca6
 
b1dca6
 	  if (endp > startp)
b1dca6
 	    normal = normalize_codeset (startp, endp - startp);
b1dca6
 	}
b1dca6
-      else
b1dca6
-	/* This is to keep gcc quiet.  */
b1dca6
-	endp = NULL;
b1dca6
 
b1dca6
-      /* We put an additional '\0' at the end of the string because at
b1dca6
-	 the end of the function we need another byte for the trailing
b1dca6
-	 '/'.  */
b1dca6
-      ssize_t n;
b1dca6
       if (normal == NULL)
b1dca6
-	n = asprintf (&result, "%s%s/%s%c", output_prefix ?: "",
b1dca6
-		      COMPLOCALEDIR, path, '\0');
b1dca6
+	result = xasprintf ("%s%s/%s/", output_prefix ?: "",
b1dca6
+			    COMPLOCALEDIR, path);
b1dca6
       else
b1dca6
-	n = asprintf (&result, "%s%s/%.*s%s%s%c",
b1dca6
-		      output_prefix ?: "", COMPLOCALEDIR,
b1dca6
-		      (int) (startp - path), path, normal, endp, '\0');
b1dca6
-
b1dca6
-      if (n < 0)
b1dca6
-	return NULL;
b1dca6
-
b1dca6
-      endp = result + n - 1;
b1dca6
+	result = xasprintf ("%s%s/%.*s%s%s/",
b1dca6
+			    output_prefix ?: "", COMPLOCALEDIR,
b1dca6
+			    (int) (startp - path), path, normal, endp ?: "");
b1dca6
+      /* Free the allocated normalized codeset name.  */
b1dca6
+      free (normal);
b1dca6
     }
b1dca6
   else
b1dca6
     {
b1dca6
-      /* This is a user path.  Please note the additional byte in the
b1dca6
-	 memory allocation.  */
b1dca6
-      size_t len = strlen (path) + 1;
b1dca6
-      result = xmalloc (len + 1);
b1dca6
-      endp = mempcpy (result, path, len) - 1;
b1dca6
+      /* This is a user path.  */
b1dca6
+      result = xasprintf ("%s/", path);
b1dca6
 
b1dca6
       /* If the user specified an output path we cannot add the output
b1dca6
 	 to the archive.  */
b1dca6
@@ -536,25 +525,41 @@ construct_output_path (char *path)
b1dca6
   errno = 0;
b1dca6
 
b1dca6
   if (no_archive && euidaccess (result, W_OK) == -1)
b1dca6
-    /* Perhaps the directory does not exist now.  Try to create it.  */
b1dca6
-    if (errno == ENOENT)
b1dca6
-      {
b1dca6
-	errno = 0;
b1dca6
-	if (mkdir (result, 0777) < 0)
b1dca6
-	  return NULL;
b1dca6
-      }
b1dca6
-
b1dca6
-  *endp++ = '/';
b1dca6
-  *endp = '\0';
b1dca6
+    {
b1dca6
+      /* Perhaps the directory does not exist now.  Try to create it.  */
b1dca6
+      if (errno == ENOENT)
b1dca6
+	{
b1dca6
+	  errno = 0;
b1dca6
+	  if (mkdir (result, 0777) < 0)
b1dca6
+	    {
b1dca6
+	      record_verbose (stderr,
b1dca6
+			      _("cannot create output path \'%s\': %s"),
b1dca6
+			      result, strerror (errno));
b1dca6
+	      free (result);
b1dca6
+	      return NULL;
b1dca6
+	    }
b1dca6
+	}
b1dca6
+      else
b1dca6
+	record_verbose (stderr,
b1dca6
+			_("no write permission to output path \'%s\': %s"),
b1dca6
+			result, strerror (errno));
b1dca6
+    }
b1dca6
 
b1dca6
   return result;
b1dca6
 }
b1dca6
 
b1dca6
 
b1dca6
-/* Normalize codeset name.  There is no standard for the codeset
b1dca6
-   names.  Normalization allows the user to use any of the common
b1dca6
-   names.  */
b1dca6
-static const char *
b1dca6
+/* Normalize codeset name.  There is no standard for the codeset names.
b1dca6
+   Normalization allows the user to use any of the common names e.g. UTF-8,
b1dca6
+   utf-8, utf8, UTF8 etc.
b1dca6
+
b1dca6
+   We normalize using the following rules:
b1dca6
+   - Remove all non-alpha-numeric characters
b1dca6
+   - Lowercase all characters.
b1dca6
+   - If there are only digits assume it's an ISO standard and prefix with 'iso'
b1dca6
+
b1dca6
+   We return the normalized string which needs to be freed by free.  */
b1dca6
+static char *
b1dca6
 normalize_codeset (const char *codeset, size_t name_len)
b1dca6
 {
b1dca6
   int len = 0;
b1dca6
@@ -563,6 +568,7 @@ normalize_codeset (const char *codeset, size_t name_len)
b1dca6
   char *wp;
b1dca6
   size_t cnt;
b1dca6
 
b1dca6
+  /* Compute the length of only the alpha-numeric characters.  */
b1dca6
   for (cnt = 0; cnt < name_len; ++cnt)
b1dca6
     if (isalnum (codeset[cnt]))
b1dca6
       {
b1dca6
@@ -572,25 +578,24 @@ normalize_codeset (const char *codeset, size_t name_len)
b1dca6
 	  only_digit = 0;
b1dca6
       }
b1dca6
 
b1dca6
-  retval = (char *) malloc ((only_digit ? 3 : 0) + len + 1);
b1dca6
+  /* If there were only digits we assume it's an ISO standard and we will
b1dca6
+     prefix with 'iso' so include space for that.  We fill in the required
b1dca6
+     space from codeset up to the converted length.  */
b1dca6
+  wp = retval = xasprintf ("%s%.*s", only_digit ? "iso" : "", len, codeset);
b1dca6
 
b1dca6
-  if (retval != NULL)
b1dca6
-    {
b1dca6
-      if (only_digit)
b1dca6
-	wp = stpcpy (retval, "iso");
b1dca6
-      else
b1dca6
-	wp = retval;
b1dca6
-
b1dca6
-      for (cnt = 0; cnt < name_len; ++cnt)
b1dca6
-	if (isalpha (codeset[cnt]))
b1dca6
-	  *wp++ = tolower (codeset[cnt]);
b1dca6
-	else if (isdigit (codeset[cnt]))
b1dca6
-	  *wp++ = codeset[cnt];
b1dca6
+  /* Skip "iso".  */
b1dca6
+  if (only_digit)
b1dca6
+    wp += 3;
b1dca6
 
b1dca6
-      *wp = '\0';
b1dca6
-    }
b1dca6
+  /* Lowercase all characters. */
b1dca6
+  for (cnt = 0; cnt < name_len; ++cnt)
b1dca6
+    if (isalpha (codeset[cnt]))
b1dca6
+      *wp++ = tolower (codeset[cnt]);
b1dca6
+    else if (isdigit (codeset[cnt]))
b1dca6
+      *wp++ = codeset[cnt];
b1dca6
 
b1dca6
-  return (const char *) retval;
b1dca6
+  /* Return allocated and converted name for caller to free.  */
b1dca6
+  return retval;
b1dca6
 }
b1dca6
 
b1dca6
 
b1dca6
diff --git a/locale/programs/localedef.h b/locale/programs/localedef.h
b1dca6
index 0083faceabbf3dd9..c528dbb97854dbd1 100644
b1dca6
--- a/locale/programs/localedef.h
b1dca6
+++ b/locale/programs/localedef.h
b1dca6
@@ -122,6 +122,7 @@ extern const char *alias_file;
b1dca6
 
b1dca6
 /* Prototypes for a few program-wide used functions.  */
b1dca6
 #include <programs/xmalloc.h>
b1dca6
+#include <programs/xasprintf.h>
b1dca6
 
b1dca6
 
b1dca6
 /* Mark given locale as to be read.  */
b1dca6
diff --git a/locale/programs/xasprintf.c b/locale/programs/xasprintf.c
b1dca6
new file mode 100644
b1dca6
index 0000000000000000..efc91a9c34074736
b1dca6
--- /dev/null
b1dca6
+++ b/locale/programs/xasprintf.c
b1dca6
@@ -0,0 +1,34 @@
b1dca6
+/* asprintf with out of memory checking
b1dca6
+   Copyright (C) 2019 Free Software Foundation, Inc.
b1dca6
+   This file is part of the GNU C Library.
b1dca6
+
b1dca6
+   This program is free software; you can redistribute it and/or modify
b1dca6
+   it under the terms of the GNU General Public License as published
b1dca6
+   by the Free Software Foundation; version 2 of the License, or
b1dca6
+   (at your option) any later version.
b1dca6
+
b1dca6
+   This program is distributed in the hope that it will be useful,
b1dca6
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
b1dca6
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
b1dca6
+   GNU General Public License for more details.
b1dca6
+
b1dca6
+   You should have received a copy of the GNU General Public License
b1dca6
+   along with this program; if not, see <https://www.gnu.org/licenses/>.  */
b1dca6
+
b1dca6
+#include <stdlib.h>
b1dca6
+#include <stdio.h>
b1dca6
+#include <stdarg.h>
b1dca6
+#include <libintl.h>
b1dca6
+#include <error.h>
b1dca6
+
b1dca6
+char *
b1dca6
+xasprintf (const char *format, ...)
b1dca6
+{
b1dca6
+  va_list ap;
b1dca6
+  va_start (ap, format);
b1dca6
+  char *result;
b1dca6
+  if (vasprintf (&result, format, ap) < 0)
b1dca6
+    error (EXIT_FAILURE, 0, _("memory exhausted"));
b1dca6
+  va_end (ap);
b1dca6
+  return result;
b1dca6
+}
b1dca6
diff --git a/locale/tst-localedef-path-norm.c b/locale/tst-localedef-path-norm.c
b1dca6
new file mode 100644
b1dca6
index 0000000000000000..2ef1d26f07084c68
b1dca6
--- /dev/null
b1dca6
+++ b/locale/tst-localedef-path-norm.c
b1dca6
@@ -0,0 +1,242 @@
b1dca6
+/* Test for localedef path name handling and normalization.
b1dca6
+   Copyright (C) 2019 Free Software Foundation, Inc.
b1dca6
+   This file is part of the GNU C Library.
b1dca6
+
b1dca6
+   The GNU C Library is free software; you can redistribute it and/or
b1dca6
+   modify it under the terms of the GNU Lesser General Public
b1dca6
+   License as published by the Free Software Foundation; either
b1dca6
+   version 2.1 of the License, or (at your option) any later version.
b1dca6
+
b1dca6
+   The GNU C Library is distributed in the hope that it will be useful,
b1dca6
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
b1dca6
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
b1dca6
+   Lesser General Public License for more details.
b1dca6
+
b1dca6
+   You should have received a copy of the GNU Lesser General Public
b1dca6
+   License along with the GNU C Library; if not, see
b1dca6
+   <https://www.gnu.org/licenses/>.  */
b1dca6
+
b1dca6
+/* The test runs localedef with various named paths to test for expected
b1dca6
+   behaviours dealing with codeset name normalization.  That is to say that use
b1dca6
+   of UTF-8, and it's variations, are normalized to utf8.  Likewise that values
b1dca6
+   after the @ are not normalized and left as-is.  The test needs to run
b1dca6
+   localedef with known input values and then check that the generated path
b1dca6
+   matches the expected value after normalization.  */
b1dca6
+
b1dca6
+/* Note: In some cases adding -v (verbose) to localedef changes the exit
b1dca6
+   status to a non-zero value because some warnings are only enabled in verbose
b1dca6
+   mode.  This should probably be changed so warnings are either present or not
b1dca6
+   present, regardless of verbosity.  POSIX requires that any warnings cause the
b1dca6
+   exit status to be non-zero.  */
b1dca6
+
b1dca6
+#include <sys/types.h>
b1dca6
+#include <sys/stat.h>
b1dca6
+#include <unistd.h>
b1dca6
+
b1dca6
+#include <support/capture_subprocess.h>
b1dca6
+#include <support/check.h>
b1dca6
+#include <support/support.h>
b1dca6
+#include <support/xunistd.h>
b1dca6
+
b1dca6
+/* Full path to localedef.  */
b1dca6
+char *prog;
b1dca6
+
b1dca6
+/* Execute localedef in a subprocess.  */
b1dca6
+static void
b1dca6
+execv_wrapper (void *args)
b1dca6
+{
b1dca6
+  char **argv = args;
b1dca6
+
b1dca6
+  execv (prog, argv);
b1dca6
+  FAIL_EXIT1 ("execv: %m");
b1dca6
+}
b1dca6
+
b1dca6
+struct test_closure
b1dca6
+{
b1dca6
+  /* Arguments for running localedef.  */
b1dca6
+  const char *const argv[16];
b1dca6
+  /* Expected directory name for compiled locale.  */
b1dca6
+  const char *exp;
b1dca6
+  /* Expected path to compiled locale.  */
b1dca6
+  const char *complocaledir;
b1dca6
+};
b1dca6
+
b1dca6
+/* Run localedef with DATA.ARGV arguments (NULL terminated), and expect path to
b1dca6
+   the compiled locale is "DATA.COMPLOCALEDIR/DATA.EXP".  */
b1dca6
+static void
b1dca6
+run_test (struct test_closure data)
b1dca6
+{
b1dca6
+  const char * const *args = data.argv;
b1dca6
+  const char *exp = data.exp;
b1dca6
+  const char *complocaledir = data.complocaledir;
b1dca6
+  struct stat64 fs;
b1dca6
+
b1dca6
+  /* Expected output path.  */
b1dca6
+  const char *path = xasprintf ("%s/%s", complocaledir, exp);
b1dca6
+
b1dca6
+  /* Run test.  */
b1dca6
+  struct support_capture_subprocess result;
b1dca6
+  result = support_capture_subprocess (execv_wrapper, (void *)args);
b1dca6
+  support_capture_subprocess_check (&result, "execv", 0, sc_allow_none);
b1dca6
+  support_capture_subprocess_free (&result);
b1dca6
+
b1dca6
+  /* Verify path is present and is a directory.  */
b1dca6
+  xstat (path, &fs);
b1dca6
+  TEST_VERIFY_EXIT (S_ISDIR (fs.st_mode));
b1dca6
+  printf ("info: Directory '%s' exists.\n", path);
b1dca6
+}
b1dca6
+
b1dca6
+static int
b1dca6
+do_test (void)
b1dca6
+{
b1dca6
+  /* We are running as root inside the container.  */
b1dca6
+  prog = xasprintf ("%s/localedef", support_bindir_prefix);
b1dca6
+
b1dca6
+  /* Create the needed directories:
b1dca6
+     - We need the default compiled locale dir for default output.
b1dca6
+     - We need an arbitrary absolute path for localedef output.
b1dca6
+
b1dca6
+     Note: Writing to a non-default absolute path disables any kind
b1dca6
+     of path normalization since we expect the user wants the path
b1dca6
+     exactly as they specified it.  */
b1dca6
+  xmkdirp (support_complocaledir_prefix, 0777);
b1dca6
+  xmkdirp ("/output", 0777);
b1dca6
+
b1dca6
+  /* It takes ~10 seconds to serially execute 9 localedef test.  We
b1dca6
+     could run the compilations in parallel if we want to reduce test
b1dca6
+     time.  We don't want to split this out into distinct tests because
b1dca6
+     it would require multiple chroots.  Batching the same localedef
b1dca6
+     tests saves disk space during testing.  */
b1dca6
+
b1dca6
+  /* Test 1: Expected normalization.
b1dca6
+     Run localedef and expect output in /usr/lib/locale/en_US1.utf8,
b1dca6
+     with normalization changing UTF-8 to utf8.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"en_US1.UTF-8", NULL },
b1dca6
+	      .exp = "en_US1.utf8",
b1dca6
+	      .complocaledir = support_complocaledir_prefix
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 2: No normalization past '@'.
b1dca6
+     Run localedef and expect output in /usr/lib/locale/en_US2.utf8@tEsT,
b1dca6
+     with normalization changing UTF-8@tEsT to utf8@tEsT (everything after
b1dca6
+     @ is untouched).  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"en_US2.UTF-8@tEsT", NULL },
b1dca6
+	      .exp = "en_US2.utf8@tEsT",
b1dca6
+	      .complocaledir = support_complocaledir_prefix
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 3: No normalization past '@' despite period.
b1dca6
+     Run localedef and expect output in /usr/lib/locale/en_US3@tEsT.UTF-8,
b1dca6
+     with normalization changing nothing (everything after @ is untouched)
b1dca6
+     despite there being a period near the end.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"en_US3@tEsT.UTF-8", NULL },
b1dca6
+	      .exp = "en_US3@tEsT.UTF-8",
b1dca6
+	      .complocaledir = support_complocaledir_prefix
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 4: Normalize numeric codeset by adding 'iso' prefix.
b1dca6
+     Run localedef and expect output in /usr/lib/locale/en_US4.88591,
b1dca6
+     with normalization changing 88591 to iso88591.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"en_US4.88591", NULL },
b1dca6
+	      .exp = "en_US4.iso88591",
b1dca6
+	      .complocaledir = support_complocaledir_prefix
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 5: Don't add 'iso' prefix if first char is alpha.
b1dca6
+     Run localedef and expect output in /usr/lib/locale/en_US5.a88591,
b1dca6
+     with normalization changing nothing.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"en_US5.a88591", NULL },
b1dca6
+	      .exp = "en_US5.a88591",
b1dca6
+	      .complocaledir = support_complocaledir_prefix
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 6: Don't add 'iso' prefix if last char is alpha.
b1dca6
+     Run localedef and expect output in /usr/lib/locale/en_US6.88591a,
b1dca6
+     with normalization changing nothing.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"en_US6.88591a", NULL },
b1dca6
+	      .exp = "en_US6.88591a",
b1dca6
+	      .complocaledir = support_complocaledir_prefix
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 7: Don't normalize anything with an absolute path.
b1dca6
+     Run localedef and expect output in /output/en_US7.UTF-8,
b1dca6
+     with normalization changing nothing.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"/output/en_US7.UTF-8", NULL },
b1dca6
+	      .exp = "en_US7.UTF-8",
b1dca6
+	      .complocaledir = "/output"
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 8: Don't normalize anything with an absolute path.
b1dca6
+     Run localedef and expect output in /output/en_US8.UTF-8@tEsT,
b1dca6
+     with normalization changing nothing.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"/output/en_US8.UTF-8@tEsT", NULL },
b1dca6
+	      .exp = "en_US8.UTF-8@tEsT",
b1dca6
+	      .complocaledir = "/output"
b1dca6
+	    });
b1dca6
+
b1dca6
+  /* Test 9: Don't normalize anything with an absolute path.
b1dca6
+     Run localedef and expect output in /output/en_US9@tEsT.UTF-8,
b1dca6
+     with normalization changing nothing.  */
b1dca6
+  run_test ((struct test_closure)
b1dca6
+	    {
b1dca6
+	      .argv = { prog,
b1dca6
+			"--no-archive",
b1dca6
+			"-i", "en_US",
b1dca6
+			"-f", "UTF-8",
b1dca6
+			"/output/en_US9@tEsT.UTF-8", NULL },
b1dca6
+	      .exp = "en_US9@tEsT.UTF-8",
b1dca6
+	      .complocaledir = "/output"
b1dca6
+	    });
b1dca6
+
b1dca6
+  return 0;
b1dca6
+}
b1dca6
+
b1dca6
+#include <support/test-driver.c>
b1dca6
diff --git a/locale/tst-localedef-path-norm.root/postclean.req b/locale/tst-localedef-path-norm.root/postclean.req
b1dca6
new file mode 100644
b1dca6
index 0000000000000000..e69de29bb2d1d643
b1dca6
diff --git a/locale/tst-localedef-path-norm.root/tst-localedef-path-norm.script b/locale/tst-localedef-path-norm.root/tst-localedef-path-norm.script
b1dca6
new file mode 100644
b1dca6
index 0000000000000000..b0f016256a47f762
b1dca6
--- /dev/null
b1dca6
+++ b/locale/tst-localedef-path-norm.root/tst-localedef-path-norm.script
b1dca6
@@ -0,0 +1,2 @@
b1dca6
+# Must run localedef as root to write into default paths.
b1dca6
+su
b1dca6
diff --git a/support/Makefile b/support/Makefile
b1dca6
index 117cfdd4f22fc405..5808a42dce87151f 100644
b1dca6
--- a/support/Makefile
b1dca6
+++ b/support/Makefile
b1dca6
@@ -182,7 +182,8 @@ CFLAGS-support_paths.c = \
b1dca6
 		-DLIBDIR_PATH=\"$(libdir)\" \
b1dca6
 		-DBINDIR_PATH=\"$(bindir)\" \
b1dca6
 		-DSBINDIR_PATH=\"$(sbindir)\" \
b1dca6
-		-DROOTSBINDIR_PATH=\"$(rootsbindir)\"
b1dca6
+		-DROOTSBINDIR_PATH=\"$(rootsbindir)\" \
b1dca6
+		-DCOMPLOCALEDIR_PATH=\"$(complocaledir)\"
b1dca6
 
b1dca6
 ifeq (,$(CXX))
b1dca6
 LINKS_DSO_PROGRAM = links-dso-program-c
b1dca6
diff --git a/support/support.h b/support/support.h
b1dca6
index 121cc9e9b7c98ca6..3af87f85fe1b762d 100644
b1dca6
--- a/support/support.h
b1dca6
+++ b/support/support.h
b1dca6
@@ -112,6 +112,8 @@ extern const char support_bindir_prefix[];
b1dca6
 extern const char support_sbindir_prefix[];
b1dca6
 /* Corresponds to the install's sbin/ directory (without prefix).  */
b1dca6
 extern const char support_install_rootsbindir[];
b1dca6
+/* Corresponds to the install's compiled locale directory.  */
b1dca6
+extern const char support_complocaledir_prefix[];
b1dca6
 
b1dca6
 extern ssize_t support_copy_file_range (int, off64_t *, int, off64_t *,
b1dca6
 					size_t, unsigned int);
b1dca6
diff --git a/support/support_paths.c b/support/support_paths.c
b1dca6
index eb2390227433aa70..6b15fae0f0173b1e 100644
b1dca6
--- a/support/support_paths.c
b1dca6
+++ b/support/support_paths.c
b1dca6
@@ -78,3 +78,10 @@ const char support_install_rootsbindir[] = ROOTSBINDIR_PATH;
b1dca6
 #else
b1dca6
 # error please -DROOTSBINDIR_PATH=something in the Makefile
b1dca6
 #endif
b1dca6
+
b1dca6
+#ifdef COMPLOCALEDIR_PATH
b1dca6
+/* Corresponds to the install's compiled locale directory.  */
b1dca6
+const char support_complocaledir_prefix[] = COMPLOCALEDIR_PATH;
b1dca6
+#else
b1dca6
+# error please -DCOMPLOCALEDIR_PATH=something in the Makefile
b1dca6
+#endif