@@ -78,8 +78,9 @@ cpp-srcs-left := $(iconv_prog-modules) $(iconvconfig-modules)
lib := iconvprogs
include $(patsubst %,$(..)libof-iterator.mk,$(cpp-srcs-left))
+tests-container += tst-iconvconfig-cache
+
ifeq ($(run-built-tests),yes)
-xtests-special += $(objpfx)test-iconvconfig.out
tests-special += \
$(objpfx)tst-iconv_prog-buffer-large.out \
$(objpfx)tst-iconv_prog-buffer-tiny.out \
@@ -129,16 +130,6 @@ $(inst_bindir)/iconv: $(objpfx)iconv_prog $(+force)
$(objpfx)iconv_prog: $(iconv_prog-modules:%=$(objpfx)%.o)
$(objpfx)iconvconfig: $(iconvconfig-modules:%=$(objpfx)%.o)
-$(objpfx)test-iconvconfig.out: $(objpfx)iconvconfig
- (set -e; \
- tmp=$(objpfx)gconv-modules.cache.$$$$; \
- rm -f $$tmp; \
- $(run-program-prefix) $(objpfx)iconvconfig \
- --output=$$tmp --nostdlib $(inst_gconvdir); \
- cmp $$tmp $(inst_gconvdir)/gconv-modules.cache; \
- rm -f $$tmp) > $@; \
- $(evaluate-test)
-
$(objpfx)tst-iconv_prog.out: tst-iconv_prog.sh $(objpfx)iconv_prog \
$(gen-locales)
$(BASH) $< $(common-objdir) '$(test-wrapper-env)' \
new file mode 100644
@@ -0,0 +1,192 @@
+/* Test that iconvconfig produces a valid, reproducible module cache.
+ Copyright (C) 2026 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <fcntl.h>
+#include <iconv.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <support/capture_subprocess.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/temp_file.h>
+#include <support/xunistd.h>
+
+#define GCONV_CACHE_MAGIC 0x20010324
+
+static char *iconvconfig;
+static char *gconvdir;
+
+static void
+run_iconvconfig (const char *out)
+{
+ char *arg_output = xasprintf ("--output=%s", out);
+ char *const argv[] =
+ {
+ iconvconfig, arg_output, (char *) "--nostdlib", gconvdir, NULL
+ };
+ struct support_capture_subprocess proc
+ = support_capture_subprogram (iconvconfig, argv, NULL);
+ support_capture_subprocess_check (&proc, "iconvconfig", 0, sc_allow_none);
+ support_capture_subprocess_free (&proc);
+ free (arg_output);
+}
+
+struct cache_file_t
+{
+ char *data;
+ size_t length;
+};
+
+static struct cache_file_t
+read_cache_file (const char *path)
+{
+ struct stat64 st;
+ xstat64 (path, &st);
+ char *buffer = xmalloc (st.st_size);
+ int fd = xopen (path, O_RDONLY, 0);
+ xread (fd, buffer, st.st_size);
+ xclose (fd);
+ return (struct cache_file_t) { buffer, st.st_size };
+}
+
+static void
+free_cache_file (struct cache_file_t *cache_file)
+{
+ free (cache_file->data);
+}
+
+static void
+write_cache_file (const char *path, const char *data, size_t length)
+{
+ int fd = xopen (path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ xwrite (fd, data, length);
+ xclose (fd);
+}
+
+static void
+move_cache_file (const char *from, const char *to)
+{
+ if (access (from, F_OK) != 0)
+ FAIL_EXIT1 ("access(%s): %m", from);
+ if (rename (from, to) != 0)
+ FAIL_EXIT1 ("rename(%s, %s): %m", from, to);
+}
+
+static void
+check_valid_cache (const struct cache_file_t *cache_file)
+{
+ TEST_VERIFY_EXIT (cache_file->length >= sizeof (uint32_t));
+ uint32_t magic;
+ memcpy (&magic, cache_file->data, sizeof (magic));
+ TEST_COMPARE (magic, GCONV_CACHE_MAGIC);
+}
+
+/* Install the generated cache CAHCE_FILE at the canonical gconv cache path
+ and confirm that the runtime loads it and resolves a module-backed
+ conversion. The gconv-modules text configuration is moved aside, so the
+ conversion can only succeed if it is satisfied from the cache. A corrupt
+ cache would make the loader fall back to the missing text configuration
+ and the conversion would fail. */
+static void
+check_loadable_cache (const struct cache_file_t *cache_file)
+{
+ char *cache = xasprintf ("%s/gconv-modules.cache", gconvdir);
+ char *conf = xasprintf ("%s/gconv-modules", gconvdir);
+ char *confd = xasprintf ("%s/gconv-modules.d", gconvdir);
+ char *conf_bak = xasprintf ("%s.disabled", conf);
+ char *confd_bak = xasprintf ("%s.disabled", confd);
+
+ write_cache_file (cache, cache_file->data, cache_file->length);
+
+ /* The runtime bypasses the cache when GCONV_PATH is set. */
+ unsetenv ("GCONV_PATH");
+
+ move_cache_file (conf, conf_bak);
+ move_cache_file (confd, confd_bak);
+
+ /* Force a module resolution, which here can only come from the cache. */
+ iconv_t cd = iconv_open ("EUC-JP", "UTF-8");
+ TEST_VERIFY (cd != (iconv_t) -1);
+
+ {
+ /* ASCII is a subset of EUC-JP, so 'A' converts to itself. */
+ char in[] = "A";
+ char out[8];
+ char *inp = in;
+ char *outp = out;
+ size_t inleft = 1;
+ size_t outleft = sizeof (out);
+ TEST_VERIFY (iconv (cd, &inp, &inleft, &outp, &outleft) != (size_t) -1);
+ TEST_COMPARE (outp - out, 1);
+ TEST_COMPARE (out[0], 'A');
+ TEST_COMPARE (iconv_close (cd), 0);
+ }
+
+ /* Restore the configuration for any later use of the container. */
+ TEST_COMPARE (rename (conf_bak, conf), 0);
+ TEST_COMPARE (rename (confd_bak, confd), 0);
+
+ free (cache);
+ free (conf);
+ free (confd);
+ free (conf_bak);
+ free (confd_bak);
+}
+
+static int
+do_test (void)
+{
+ iconvconfig = xasprintf ("%s/iconvconfig", support_sbindir_prefix);
+ gconvdir = xasprintf ("%s/gconv", support_libdir_prefix);
+
+ char *cache1;
+ xclose (create_temp_file ("iconvconfig-cache1-", &cache1));
+ char *cache2;
+ xclose (create_temp_file ("iconvconfig-cache2-", &cache2));
+
+ run_iconvconfig (cache1);
+ run_iconvconfig (cache2);
+
+ struct cache_file_t cache_file1 = read_cache_file (cache1);
+ struct cache_file_t cache_file2 = read_cache_file (cache2);
+
+ /* Check if the cache generation is deterministic. */
+ TEST_VERIFY (cache_file1.length > 0);
+ TEST_COMPARE (cache_file1.length, cache_file2.length);
+ TEST_COMPARE_BLOB (cache_file1.data, cache_file1.length,
+ cache_file1.data, cache_file1.length);
+
+ /* And well-formed. */
+ check_valid_cache (&cache_file1);
+
+ /* And the runtime must be able to load it and convert through it. */
+ check_loadable_cache (&cache_file2);
+
+ free_cache_file (&cache_file1);
+ free_cache_file (&cache_file2);
+ free (iconvconfig);
+ free (gconvdir);
+
+ return 0;
+}
+
+#include <support/test-driver.c>
new file mode 100644
new file mode 100644