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