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