--- /dev/null
+From 5a842672e79a7a5f6be837c483be4f9901a4ecc0 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Wed, 30 Apr 2025 03:19:10 +0200
+Subject: [PATCH] New module mem-hash-map.
+
+* lib/mem-hash-map.h: New file, from GNU gettext.
+* lib/mem-hash-map.c: New file, from GNU gettext.
+* modules/mem-hash-map: New file, from GNU gettext.
+---
+ ChangeLog | 7 +
+ lib/mem-hash-map.c | 352 +++++++++++++++++++++++++++++++++++++++++++
+ lib/mem-hash-map.h | 90 +++++++++++
+ modules/mem-hash-map | 25 +++
+ 4 files changed, 474 insertions(+)
+ create mode 100644 lib/mem-hash-map.c
+ create mode 100644 lib/mem-hash-map.h
+ create mode 100644 modules/mem-hash-map
+
+--- /dev/null
++++ b/lib/mem-hash-map.c
+@@ -0,0 +1,352 @@
++/* Simple hash table (no removals) where the keys are memory blocks.
++ Copyright (C) 1994-2025 Free Software Foundation, Inc.
++ Written by Ulrich Drepper <drepper@gnu.ai.mit.edu>, October 1994.
++
++ This file is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published
++ by the Free Software Foundation, either version 3 of the License,
++ or (at your option) any later version.
++
++ This file 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 General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with this program. If not, see <https://www.gnu.org/licenses/>. */
++
++#include <config.h>
++
++/* Specification. */
++#include "mem-hash-map.h"
++
++#include <stdlib.h>
++#include <string.h>
++#include <stdio.h>
++#include <limits.h>
++#include <sys/types.h>
++
++#include "next-prime.h"
++
++/* Since this simple implementation of hash tables allows only insertion, no
++ removal of entries, the right data structure for the memory holding all keys
++ is an obstack. */
++#include "obstack.h"
++
++/* Use checked memory allocation. */
++#include "xalloc.h"
++
++#define obstack_chunk_alloc xmalloc
++#define obstack_chunk_free free
++
++
++typedef struct hash_entry
++{
++ size_t used; /* Hash code of the key, or 0 for an unused entry. */
++ const void *key; /* Key. */
++ size_t keylen;
++ void *data; /* Value. */
++ struct hash_entry *next;
++}
++hash_entry;
++
++
++/* Initialize a hash table. INIT_SIZE > 1 is the initial number of available
++ entries.
++ Return 0 always. */
++int
++hash_init (hash_table *htab, size_t init_size)
++{
++ /* We need the size to be a prime. */
++ init_size = next_prime (init_size);
++
++ /* Initialize the data structure. */
++ htab->size = init_size;
++ htab->filled = 0;
++ htab->first = NULL;
++ htab->table = XCALLOC (init_size + 1, hash_entry);
++
++ obstack_init (&htab->mem_pool);
++
++ return 0;
++}
++
++
++/* Delete a hash table's contents.
++ Return 0 always. */
++int
++hash_destroy (hash_table *htab)
++{
++ free (htab->table);
++ obstack_free (&htab->mem_pool, NULL);
++ return 0;
++}
++
++
++/* Compute a hash code for a key consisting of KEYLEN bytes starting at KEY
++ in memory. */
++static size_t
++compute_hashval (const void *key, size_t keylen)
++{
++ size_t cnt;
++ size_t hval;
++
++ /* Compute the hash value for the given string. The algorithm
++ is taken from [Aho,Sethi,Ullman], fixed according to
++ https://haible.de/bruno/hashfunc.html. */
++ cnt = 0;
++ hval = keylen;
++ while (cnt < keylen)
++ {
++ hval = (hval << 9) | (hval >> (sizeof (size_t) * CHAR_BIT - 9));
++ hval += (size_t) *(((const char *) key) + cnt++);
++ }
++ return hval != 0 ? hval : ~((size_t) 0);
++}
++
++
++/* References:
++ [Aho,Sethi,Ullman] Compilers: Principles, Techniques and Tools, 1986
++ [Knuth] The Art of Computer Programming, part3 (6.4) */
++
++/* Look up a given key in the hash table.
++ Return the index of the entry, if present, or otherwise the index a free
++ entry where it could be inserted. */
++static size_t
++lookup (const hash_table *htab,
++ const void *key, size_t keylen,
++ size_t hval)
++{
++ size_t hash;
++ size_t idx;
++ hash_entry *table = htab->table;
++
++ /* First hash function: simply take the modul but prevent zero. */
++ hash = 1 + hval % htab->size;
++
++ idx = hash;
++
++ if (table[idx].used)
++ {
++ if (table[idx].used == hval && table[idx].keylen == keylen
++ && memcmp (table[idx].key, key, keylen) == 0)
++ return idx;
++
++ /* Second hash function as suggested in [Knuth]. */
++ hash = 1 + hval % (htab->size - 2);
++
++ do
++ {
++ if (idx <= hash)
++ idx = htab->size + idx - hash;
++ else
++ idx -= hash;
++
++ /* If entry is found use it. */
++ if (table[idx].used == hval && table[idx].keylen == keylen
++ && memcmp (table[idx].key, key, keylen) == 0)
++ return idx;
++ }
++ while (table[idx].used);
++ }
++ return idx;
++}
++
++
++/* Look up the value of a key in the given table.
++ If found, return 0 and set *RESULT to it. Otherwise return -1. */
++int
++hash_find_entry (const hash_table *htab, const void *key, size_t keylen,
++ void **result)
++{
++ hash_entry *table = htab->table;
++ size_t idx = lookup (htab, key, keylen, compute_hashval (key, keylen));
++
++ if (table[idx].used == 0)
++ return -1;
++
++ *result = table[idx].data;
++ return 0;
++}
++
++
++/* Insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table at index IDX.
++ HVAL is the key's hash code. IDX depends on it. The table entry at index
++ IDX is known to be unused. */
++static void
++insert_entry_2 (hash_table *htab,
++ const void *key, size_t keylen,
++ size_t hval, size_t idx, void *data)
++{
++ hash_entry *table = htab->table;
++
++ table[idx].used = hval;
++ table[idx].key = key;
++ table[idx].keylen = keylen;
++ table[idx].data = data;
++
++ /* List the new value in the list. */
++ if (htab->first == NULL)
++ {
++ table[idx].next = &table[idx];
++ htab->first = &table[idx];
++ }
++ else
++ {
++ table[idx].next = htab->first->next;
++ htab->first->next = &table[idx];
++ htab->first = &table[idx];
++ }
++
++ ++htab->filled;
++}
++
++
++/* Grow the hash table. */
++static void
++resize (hash_table *htab)
++{
++ size_t old_size = htab->size;
++ hash_entry *table = htab->table;
++ size_t idx;
++
++ htab->size = next_prime (htab->size * 2);
++ htab->filled = 0;
++ htab->first = NULL;
++ htab->table = XCALLOC (1 + htab->size, hash_entry);
++
++ for (idx = 1; idx <= old_size; ++idx)
++ if (table[idx].used)
++ insert_entry_2 (htab, table[idx].key, table[idx].keylen,
++ table[idx].used,
++ lookup (htab, table[idx].key, table[idx].keylen,
++ table[idx].used),
++ table[idx].data);
++
++ free (table);
++}
++
++
++/* Try to insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++ Return non-NULL (more precisely, the address of the KEY inside the table's
++ memory pool) if successful, or NULL if there is already an entry with the
++ given key. */
++const void *
++hash_insert_entry (hash_table *htab,
++ const void *key, size_t keylen,
++ void *data)
++{
++ size_t hval = compute_hashval (key, keylen);
++ hash_entry *table = htab->table;
++ size_t idx = lookup (htab, key, keylen, hval);
++
++ if (table[idx].used)
++ /* We don't want to overwrite the old value. */
++ return NULL;
++ else
++ {
++ /* An empty bucket has been found. */
++ void *keycopy = obstack_copy (&htab->mem_pool, key, keylen);
++ insert_entry_2 (htab, keycopy, keylen, hval, idx, data);
++ if (100 * htab->filled > 75 * htab->size)
++ /* Table is filled more than 75%. Resize the table. */
++ resize (htab);
++ return keycopy;
++ }
++}
++
++
++/* Insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++ Return 0. */
++int
++hash_set_value (hash_table *htab,
++ const void *key, size_t keylen,
++ void *data)
++{
++ size_t hval = compute_hashval (key, keylen);
++ hash_entry *table = htab->table;
++ size_t idx = lookup (htab, key, keylen, hval);
++
++ if (table[idx].used)
++ {
++ /* Overwrite the old value. */
++ table[idx].data = data;
++ return 0;
++ }
++ else
++ {
++ /* An empty bucket has been found. */
++ void *keycopy = obstack_copy (&htab->mem_pool, key, keylen);
++ insert_entry_2 (htab, keycopy, keylen, hval, idx, data);
++ if (100 * htab->filled > 75 * htab->size)
++ /* Table is filled more than 75%. Resize the table. */
++ resize (htab);
++ return 0;
++ }
++}
++
++
++/* Steps *PTR forward to the next used entry in the given hash table. *PTR
++ should be initially set to NULL. Store information about the next entry
++ in *KEY, *KEYLEN, *DATA.
++ Return 0 normally, -1 when the whole hash table has been traversed. */
++int
++hash_iterate (hash_table *htab, void **ptr, const void **key, size_t *keylen,
++ void **data)
++{
++ hash_entry *curr;
++
++ if (*ptr == NULL)
++ {
++ if (htab->first == NULL)
++ return -1;
++ curr = htab->first;
++ }
++ else
++ {
++ if (*ptr == htab->first)
++ return -1;
++ curr = (hash_entry *) *ptr;
++ }
++ curr = curr->next;
++ *ptr = (void *) curr;
++
++ *key = curr->key;
++ *keylen = curr->keylen;
++ *data = curr->data;
++ return 0;
++}
++
++
++/* Steps *PTR forward to the next used entry in the given hash table. *PTR
++ should be initially set to NULL. Store information about the next entry
++ in *KEY, *KEYLEN, *DATAP. *DATAP is set to point to the storage of the
++ value; modifying **DATAP will modify the value of the entry.
++ Return 0 normally, -1 when the whole hash table has been traversed. */
++int
++hash_iterate_modify (hash_table *htab, void **ptr,
++ const void **key, size_t *keylen,
++ void ***datap)
++{
++ hash_entry *curr;
++
++ if (*ptr == NULL)
++ {
++ if (htab->first == NULL)
++ return -1;
++ curr = htab->first;
++ }
++ else
++ {
++ if (*ptr == htab->first)
++ return -1;
++ curr = (hash_entry *) *ptr;
++ }
++ curr = curr->next;
++ *ptr = (void *) curr;
++
++ *key = curr->key;
++ *keylen = curr->keylen;
++ *datap = &curr->data;
++ return 0;
++}
+--- /dev/null
++++ b/lib/mem-hash-map.h
+@@ -0,0 +1,90 @@
++/* Simple hash table (no removals) where the keys are memory blocks.
++ Copyright (C) 1995-2025 Free Software Foundation, Inc.
++
++ This file is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published
++ by the Free Software Foundation, either version 3 of the License,
++ or (at your option) any later version.
++
++ This file 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 General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with this program. If not, see <https://www.gnu.org/licenses/>. */
++
++#ifndef _GL_MEM_HASH_MAP_H
++#define _GL_MEM_HASH_MAP_H
++
++#include <stddef.h>
++
++#include "obstack.h"
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++struct hash_entry;
++
++typedef struct hash_table
++{
++ size_t size; /* Number of allocated entries. */
++ size_t filled; /* Number of used entries. */
++ struct hash_entry *first; /* Pointer to head of list of entries. */
++ struct hash_entry *table; /* Pointer to array of entries. */
++ struct obstack mem_pool; /* Memory pool holding the keys. */
++}
++hash_table;
++
++/* Initialize a hash table. INIT_SIZE > 1 is the initial number of available
++ entries.
++ Return 0 always. */
++extern int hash_init (hash_table *htab, size_t init_size);
++
++/* Delete a hash table's contents.
++ Return 0 always. */
++extern int hash_destroy (hash_table *htab);
++
++/* Look up the value of a key in the given table.
++ If found, return 0 and set *RESULT to it. Otherwise return -1. */
++extern int hash_find_entry (const hash_table *htab,
++ const void *key, size_t keylen,
++ void **result);
++
++/* Try to insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++ Return non-NULL (more precisely, the address of the KEY inside the table's
++ memory pool) if successful, or NULL if there is already an entry with the
++ given key. */
++extern const void * hash_insert_entry (hash_table *htab,
++ const void *key, size_t keylen,
++ void *data);
++
++/* Insert the pair (KEY[0..KEYLEN-1], DATA) in the hash table.
++ Return 0. */
++extern int hash_set_value (hash_table *htab,
++ const void *key, size_t keylen,
++ void *data);
++
++/* Steps *PTR forward to the next used entry in the given hash table. *PTR
++ should be initially set to NULL. Store information about the next entry
++ in *KEY, *KEYLEN, *DATA.
++ Return 0 normally, -1 when the whole hash table has been traversed. */
++extern int hash_iterate (hash_table *htab, void **ptr,
++ const void **key, size_t *keylen,
++ void **data);
++
++/* Steps *PTR forward to the next used entry in the given hash table. *PTR
++ should be initially set to NULL. Store information about the next entry
++ in *KEY, *KEYLEN, *DATAP. *DATAP is set to point to the storage of the
++ value; modifying **DATAP will modify the value of the entry.
++ Return 0 normally, -1 when the whole hash table has been traversed. */
++extern int hash_iterate_modify (hash_table *htab, void **ptr,
++ const void **key, size_t *keylen,
++ void ***datap);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* not _GL_MEM_HASH_MAP_H */
+--- /dev/null
++++ b/modules/mem-hash-map
+@@ -0,0 +1,25 @@
++Description:
++Simple hash table (no removals) where the keys are memory blocks.
++
++Files:
++lib/mem-hash-map.h
++lib/mem-hash-map.c
++
++Depends-on:
++next-prime
++obstack
++xalloc
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += mem-hash-map.h mem-hash-map.c
++
++Include:
++"mem-hash-map.h"
++
++License:
++GPL
++
++Maintainer:
++Bruno Haible
--- /dev/null
+From 0b953ba82830f51ce9b939700705d238f9b0c0ba Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Wed, 30 Apr 2025 01:52:17 +0200
+Subject: [PATCH] New module next-prime.
+
+* lib/next-prime.h: New file, based on lib/hash.c.
+* lib/next-prime.c: New file, based on lib/hash.c.
+* modules/next-prime: New file.
+* lib/hash.c: Include next-prime.h.
+(is_prime, next_prime): Remove functions.
+* modules/hash (Depends-on): Add next-prime.
+---
+ ChangeLog | 10 +++++++++
+ lib/hash.c | 39 +-------------------------------
+ lib/next-prime.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++
+ lib/next-prime.h | 41 +++++++++++++++++++++++++++++++++
+ modules/hash | 1 +
+ modules/next-prime | 24 ++++++++++++++++++++
+ 6 files changed, 133 insertions(+), 38 deletions(-)
+ create mode 100644 lib/next-prime.c
+ create mode 100644 lib/next-prime.h
+ create mode 100644 modules/next-prime
+
+--- a/lib/hash.c
++++ b/lib/hash.c
+@@ -27,6 +27,7 @@
+ #include "hash.h"
+
+ #include "bitrotate.h"
++#include "next-prime.h"
+ #include "xalloc-oversized.h"
+
+ #include <errno.h>
+@@ -390,44 +391,6 @@ hash_string (const char *string, size_t
+
+ #endif /* not USE_DIFF_HASH */
+
+-/* Return true if CANDIDATE is a prime number. CANDIDATE should be an odd
+- number at least equal to 11. */
+-
+-static bool _GL_ATTRIBUTE_CONST
+-is_prime (size_t candidate)
+-{
+- size_t divisor = 3;
+- size_t square = divisor * divisor;
+-
+- while (square < candidate && (candidate % divisor))
+- {
+- divisor++;
+- square += 4 * divisor;
+- divisor++;
+- }
+-
+- return (candidate % divisor ? true : false);
+-}
+-
+-/* Round a given CANDIDATE number up to the nearest prime, and return that
+- prime. Primes lower than 10 are merely skipped. */
+-
+-static size_t _GL_ATTRIBUTE_CONST
+-next_prime (size_t candidate)
+-{
+- /* Skip small primes. */
+- if (candidate < 10)
+- candidate = 10;
+-
+- /* Make it definitely odd. */
+- candidate |= 1;
+-
+- while (SIZE_MAX != candidate && !is_prime (candidate))
+- candidate += 2;
+-
+- return candidate;
+-}
+-
+ void
+ hash_reset_tuning (Hash_tuning *tuning)
+ {
+--- /dev/null
++++ b/lib/next-prime.c
+@@ -0,0 +1,56 @@
++/* Finding the next prime >= a given small integer.
++ Copyright (C) 1995-2025 Free Software Foundation, Inc.
++
++ This file 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.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++#include <config.h>
++
++/* Specification. */
++#include "next-prime.h"
++
++#include <stdint.h> /* for SIZE_MAX */
++
++/* Return true if CANDIDATE is a prime number. CANDIDATE should be an odd
++ number at least equal to 11. */
++static bool _GL_ATTRIBUTE_CONST
++is_prime (size_t candidate)
++{
++ size_t divisor = 3;
++ size_t square = divisor * divisor;
++
++ while (square < candidate && (candidate % divisor))
++ {
++ divisor++;
++ square += 4 * divisor;
++ divisor++;
++ }
++
++ return (candidate % divisor ? true : false);
++}
++
++size_t _GL_ATTRIBUTE_CONST
++next_prime (size_t candidate)
++{
++ /* Skip small primes. */
++ if (candidate < 10)
++ candidate = 10;
++
++ /* Make it definitely odd. */
++ candidate |= 1;
++
++ while (SIZE_MAX != candidate && !is_prime (candidate))
++ candidate += 2;
++
++ return candidate;
++}
+--- /dev/null
++++ b/lib/next-prime.h
+@@ -0,0 +1,41 @@
++/* Finding the next prime >= a given small integer.
++ Copyright (C) 1995-2025 Free Software Foundation, Inc.
++
++ This file 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.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++#ifndef _GL_NEXT_PRIME_H
++#define _GL_NEXT_PRIME_H
++
++/* This file uses _GL_ATTRIBUTE_CONST. */
++#if !_GL_CONFIG_H_INCLUDED
++ #error "Please include config.h first."
++#endif
++
++#include <stddef.h>
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++
++/* Round a given CANDIDATE number up to the nearest prime, and return that
++ prime. Primes lower than 10 are merely skipped. */
++extern size_t _GL_ATTRIBUTE_CONST next_prime (size_t candidate);
++
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* _GL_NEXT_PRIME_H */
+--- a/modules/hash
++++ b/modules/hash
+@@ -10,6 +10,7 @@ bitrotate
+ calloc-posix
+ free-posix
+ malloc-posix
++next-prime
+ bool
+ stdint-h
+ xalloc-oversized
+--- /dev/null
++++ b/modules/next-prime
+@@ -0,0 +1,24 @@
++Description:
++Finding the next prime >= a given small integer.
++
++Files:
++lib/next-prime.h
++lib/next-prime.c
++
++Depends-on:
++bool
++stdint-h
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += next-prime.h next-prime.c
++
++Include:
++"next-prime.h"
++
++License:
++LGPLv2+
++
++Maintainer:
++all
--- /dev/null
+From 64042bb91aea5f854ca8a8938e2b3f7d1935e4f1 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Wed, 30 Apr 2025 12:47:37 +0200
+Subject: [PATCH] New module hashcode-string1.
+
+* lib/hashcode-string1.h: New file.
+* lib/hashcode-string1.c: New file, based on lib/hash.c.
+* modules/hashcode-string1: New file.
+* lib/hash.h: Include hashcode-string1.h.
+(hash_string): Remove declaration.
+* lib/hash.c (hash_string): Remove function.
+* modules/hash (Depends-on): Add hashcode-string1.
+* lib/exclude.c: Include hashcode-string1.h.
+* modules/exclude (Depends-on): Add hashcode-string1.
+---
+ ChangeLog | 13 +++++++++
+ lib/exclude.c | 1 +
+ lib/hash.c | 59 ++++++--------------------------------
+ lib/hash.h | 11 +++----
+ lib/hashcode-string1.c | 62 ++++++++++++++++++++++++++++++++++++++++
+ lib/hashcode-string1.h | 38 ++++++++++++++++++++++++
+ modules/exclude | 1 +
+ modules/hash | 1 +
+ modules/hashcode-string1 | 24 ++++++++++++++++
+ 9 files changed, 154 insertions(+), 56 deletions(-)
+ create mode 100644 lib/hashcode-string1.c
+ create mode 100644 lib/hashcode-string1.h
+ create mode 100644 modules/hashcode-string1
+
+--- a/lib/exclude.c
++++ b/lib/exclude.c
+@@ -36,6 +36,7 @@
+ #include "filename.h"
+ #include <fnmatch.h>
+ #include "hash.h"
++#include "hashcode-string1.h"
+ #if GNULIB_MCEL_PREFER
+ # include "mcel.h"
+ #else
+--- a/lib/hash.c
++++ b/lib/hash.c
+@@ -345,57 +345,6 @@ hash_do_for_each (const Hash_table *tabl
+ return counter;
+ }
+
+-/* Allocation and clean-up. */
+-
+-#if USE_DIFF_HASH
+-
+-/* About hashings, Paul Eggert writes to me (FP), on 1994-01-01: "Please see
+- B. J. McKenzie, R. Harries & T. Bell, Selecting a hashing algorithm,
+- Software--practice & experience 20, 2 (Feb 1990), 209-224. Good hash
+- algorithms tend to be domain-specific, so what's good for [diffutils'] io.c
+- may not be good for your application." */
+-
+-size_t
+-hash_string (const char *string, size_t n_buckets)
+-{
+-# define HASH_ONE_CHAR(Value, Byte) \
+- ((Byte) + rotl_sz (Value, 7))
+-
+- size_t value = 0;
+- unsigned char ch;
+-
+- for (; (ch = *string); string++)
+- value = HASH_ONE_CHAR (value, ch);
+- return value % n_buckets;
+-
+-# undef HASH_ONE_CHAR
+-}
+-
+-#else /* not USE_DIFF_HASH */
+-
+-/* This one comes from 'recode', and performs a bit better than the above as
+- per a few experiments. It is inspired from a hashing routine found in the
+- very old Cyber 'snoop', itself written in typical Greg Mansfield style.
+- (By the way, what happened to this excellent man? Is he still alive?) */
+-
+-size_t
+-hash_string (const char *string, size_t n_buckets)
+-{
+- size_t value = 0;
+- unsigned char ch;
+-
+- for (; (ch = *string); string++)
+- value = (value * 31 + ch) % n_buckets;
+- return value;
+-}
+-
+-#endif /* not USE_DIFF_HASH */
+-
+-void
+-hash_reset_tuning (Hash_tuning *tuning)
+-{
+- *tuning = default_tuning;
+-}
+
+ /* If the user passes a NULL hasher, we hash the raw pointer. */
+ static size_t
+@@ -418,6 +367,14 @@ raw_comparator (const void *a, const voi
+ }
+
+
++/* Allocation and clean-up. */
++
++void
++hash_reset_tuning (Hash_tuning *tuning)
++{
++ *tuning = default_tuning;
++}
++
+ /* For the given hash TABLE, check the user supplied tuning structure for
+ reasonable values, and return true if there is no gross error with it.
+ Otherwise, definitively reset the TUNING field to some acceptable default
+--- a/lib/hash.h
++++ b/lib/hash.h
+@@ -134,11 +134,6 @@ extern size_t hash_do_for_each (const Ha
+ * Allocation and clean-up.
+ */
+
+-/* Return a hash index for a NUL-terminated STRING between 0 and N_BUCKETS-1.
+- This is a convenience routine for constructing other hashing functions. */
+-extern size_t hash_string (const char *string, size_t n_buckets)
+- _GL_ATTRIBUTE_PURE;
+-
+ extern void hash_reset_tuning (Hash_tuning *tuning);
+
+ typedef size_t (*Hash_hasher) (const void *entry, size_t table_size);
+@@ -266,6 +261,12 @@ extern void *hash_remove (Hash_table *ta
+ _GL_ATTRIBUTE_DEPRECATED
+ extern void *hash_delete (Hash_table *table, const void *entry);
+
++
++# if GNULIB_HASHCODE_STRING1
++/* Include declarations of module 'hashcode-string1'. */
++# include "hashcode-string1.h"
++# endif
++
+ # ifdef __cplusplus
+ }
+ # endif
+--- /dev/null
++++ b/lib/hashcode-string1.c
+@@ -0,0 +1,62 @@
++/* hashcode-string1.c -- compute a hash value from a NUL-terminated string.
++
++ Copyright (C) 1998-2004, 2006-2007, 2009-2025 Free Software Foundation, Inc.
++
++ This file 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.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++#include <config.h>
++
++/* Specification. */
++#include "hashcode-string1.h"
++
++#if USE_DIFF_HASH
++
++# include "bitrotate.h"
++
++/* About hashings, Paul Eggert writes to me (FP), on 1994-01-01: "Please see
++ B. J. McKenzie, R. Harries & T. Bell, Selecting a hashing algorithm,
++ Software--practice & experience 20, 2 (Feb 1990), 209-224. Good hash
++ algorithms tend to be domain-specific, so what's good for [diffutils'] io.c
++ may not be good for your application." */
++
++size_t
++hash_string (const char *string, size_t tablesize)
++{
++ size_t value = 0;
++ unsigned char ch;
++
++ for (; (ch = *string); string++)
++ value = ch + rotl_sz (value, 7);
++ return value % tablesize;
++}
++
++#else /* not USE_DIFF_HASH */
++
++/* This one comes from 'recode', and performs a bit better than the above as
++ per a few experiments. It is inspired from a hashing routine found in the
++ very old Cyber 'snoop', itself written in typical Greg Mansfield style.
++ (By the way, what happened to this excellent man? Is he still alive?) */
++
++size_t
++hash_string (const char *string, size_t tablesize)
++{
++ size_t value = 0;
++ unsigned char ch;
++
++ for (; (ch = *string); string++)
++ value = (value * 31 + ch) % tablesize;
++ return value;
++}
++
++#endif /* not USE_DIFF_HASH */
+--- /dev/null
++++ b/lib/hashcode-string1.h
+@@ -0,0 +1,38 @@
++/* hashcode-string1.h -- declaration for a simple hash function
++ Copyright (C) 1998-2004, 2006-2007, 2009-2025 Free Software Foundation, Inc.
++
++ This file 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.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++/* This file uses _GL_ATTRIBUTE_PURE. */
++#if !_GL_CONFIG_H_INCLUDED
++ #error "Please include config.h first."
++#endif
++
++#include <stddef.h>
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++
++/* Compute a hash code for a NUL-terminated string S,
++ and return the hash code modulo TABLESIZE.
++ The result is platform dependent: it depends on the size of the 'size_t'
++ type. */
++extern size_t hash_string (char const *s, size_t tablesize) _GL_ATTRIBUTE_PURE;
++
++
++#ifdef __cplusplus
++}
++#endif
+--- a/modules/exclude
++++ b/modules/exclude
+@@ -12,6 +12,7 @@ filename
+ fnmatch
+ fopen-gnu
+ hash
++hashcode-string1
+ mbscasecmp
+ mbuiter [test "$GNULIB_MCEL_PREFER" != yes]
+ nullptr
+--- a/modules/hash
++++ b/modules/hash
+@@ -14,6 +14,7 @@ next-prime
+ bool
+ stdint-h
+ xalloc-oversized
++hashcode-string1
+
+ configure.ac:
+
+--- /dev/null
++++ b/modules/hashcode-string1
+@@ -0,0 +1,24 @@
++Description:
++Compute a hash value for a NUL-terminated string.
++
++Files:
++lib/hashcode-string1.h
++lib/hashcode-string1.c
++
++Depends-on:
++bitrotate
++
++configure.ac:
++gl_MODULE_INDICATOR([hashcode-string1])
++
++Makefile.am:
++lib_SOURCES += hashcode-string1.h hashcode-string1.c
++
++Include:
++"hashcode-string1.h"
++
++License:
++LGPLv2+
++
++Maintainer:
++Jim Meyering
--- /dev/null
+From 52738dcd0f522b16653cc8b21adfcb758702f2ab Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Wed, 30 Apr 2025 01:20:17 +0200
+Subject: [PATCH] New module hashkey-string.
+
+* lib/hashkey-string.h: New file.
+* lib/hashkey-string.c: New file, based on lib/clean-temp-simple.c.
+* modules/hashkey-string: New file.
+* lib/clean-temp-simple.c: Include hashkey-string.h. Don't include
+<limits.h>.
+(clean_temp_string_equals, clean_temp_string_hash): Remove functions.
+(SIZE_BITS): Remove macro.
+(register_temporary_file): Use hashkey_string_equals and
+hashkey_string_hash.
+* modules/clean-temp-simple (Depends-on): Add hashkey-string.
+---
+ ChangeLog | 14 ++++++++++++
+ lib/clean-temp-simple.c | 33 +++------------------------
+ lib/hashkey-string.c | 48 +++++++++++++++++++++++++++++++++++++++
+ lib/hashkey-string.h | 35 ++++++++++++++++++++++++++++
+ modules/clean-temp-simple | 1 +
+ modules/hashkey-string | 23 +++++++++++++++++++
+ 6 files changed, 124 insertions(+), 30 deletions(-)
+ create mode 100644 lib/hashkey-string.c
+ create mode 100644 lib/hashkey-string.h
+ create mode 100644 modules/hashkey-string
+
+--- a/lib/clean-temp-simple.c
++++ b/lib/clean-temp-simple.c
+@@ -22,7 +22,6 @@
+ #include "clean-temp-private.h"
+
+ #include <errno.h>
+-#include <limits.h>
+ #include <signal.h>
+ #include <stdlib.h>
+ #include <string.h>
+@@ -36,6 +35,7 @@
+ #include "thread-optim.h"
+ #include "gl_list.h"
+ #include "gl_linkedhash_list.h"
++#include "hashkey-string.h"
+ #include "gettext.h"
+
+ #define _(msgid) dgettext ("gnulib", msgid)
+@@ -106,33 +106,6 @@ gl_list_t /* <closeable_fd *> */ volatil
+ asynchronous signal.
+ */
+
+-/* String equality and hash code functions used by the lists. */
+-
+-bool
+-clean_temp_string_equals (const void *x1, const void *x2)
+-{
+- const char *s1 = (const char *) x1;
+- const char *s2 = (const char *) x2;
+- return strcmp (s1, s2) == 0;
+-}
+-
+-#define SIZE_BITS (sizeof (size_t) * CHAR_BIT)
+-
+-/* A hash function for NUL-terminated char* strings using
+- the method described by Bruno Haible.
+- See https://www.haible.de/bruno/hashfunc.html. */
+-size_t
+-clean_temp_string_hash (const void *x)
+-{
+- const char *s = (const char *) x;
+- size_t h = 0;
+-
+- for (; *s; s++)
+- h = *s + ((h << 9) | (h >> (SIZE_BITS - 9)));
+-
+- return h;
+-}
+-
+
+ /* The set of fatal signal handlers.
+ Cached here because we are not allowed to call get_fatal_signal_set ()
+@@ -326,8 +299,8 @@ register_temporary_file (const char *abs
+ }
+ file_cleanup_list =
+ gl_list_nx_create_empty (GL_LINKEDHASH_LIST,
+- clean_temp_string_equals,
+- clean_temp_string_hash,
++ hashkey_string_equals,
++ hashkey_string_hash,
+ NULL, false);
+ if (file_cleanup_list == NULL)
+ {
+--- /dev/null
++++ b/lib/hashkey-string.c
+@@ -0,0 +1,48 @@
++/* Support for using a string as a hash key.
++ Copyright (C) 2006-2025 Free Software Foundation, Inc.
++
++ This file 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.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++#include <config.h>
++
++/* Specification. */
++#include "hashkey-string.h"
++
++#include <limits.h>
++#include <string.h>
++
++bool
++hashkey_string_equals (const void *x1, const void *x2)
++{
++ const char *s1 = (const char *) x1;
++ const char *s2 = (const char *) x2;
++ return strcmp (s1, s2) == 0;
++}
++
++#define SIZE_BITS (sizeof (size_t) * CHAR_BIT)
++
++/* A hash function for NUL-terminated 'const char *' strings using
++ the method described by Bruno Haible.
++ See https://www.haible.de/bruno/hashfunc.html. */
++size_t
++hashkey_string_hash (const void *x)
++{
++ const char *s = (const char *) x;
++ size_t h = 0;
++
++ for (; *s; s++)
++ h = *s + ((h << 9) | (h >> (SIZE_BITS - 9)));
++
++ return h;
++}
+--- /dev/null
++++ b/lib/hashkey-string.h
+@@ -0,0 +1,35 @@
++/* Support for using a string as a hash key.
++ Copyright (C) 2006-2025 Free Software Foundation, Inc.
++
++ This file 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.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++#ifndef _GL_HASHKEY_STRING_H
++#define _GL_HASHKEY_STRING_H
++
++#include <stddef.h>
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++/* String equality and hash code functions that operate on plain C strings
++ ('const char *'). */
++extern bool hashkey_string_equals (const void *x1, const void *x2);
++extern size_t hashkey_string_hash (const void *x);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* _GL_HASHKEY_STRING_H */
+--- a/modules/clean-temp-simple
++++ b/modules/clean-temp-simple
+@@ -19,6 +19,7 @@ error
+ fatal-signal
+ rmdir
+ linkedhash-list
++hashkey-string
+ gettext-h
+ gnulib-i18n
+
+--- /dev/null
++++ b/modules/hashkey-string
+@@ -0,0 +1,23 @@
++Description:
++Support for using a string as a hash key in the hash-set and hash-map modules.
++
++Files:
++lib/hashkey-string.h
++lib/hashkey-string.c
++
++Depends-on:
++bool
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += hashkey-string.h hashkey-string.c
++
++Include:
++"hashkey-string.h"
++
++License:
++LGPLv2+
++
++Maintainer:
++all
--- /dev/null
+From e518788ad085e02b046e42889039a1f671e4619a Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Wed, 22 Jan 2025 21:21:59 +0100
+Subject: New module 'package-version'.
+
+* m4/init-package-version.m4: New file, from GNU libunistring.
+* modules/package-version: New file.
+* modules/git-version-gen (Depends-on): Add it.
+---
+ ChangeLog | 7 +++
+ m4/init-package-version.m4 | 124 +++++++++++++++++++++++++++++++++++++++++++++
+ modules/git-version-gen | 1 +
+ modules/package-version | 19 +++++++
+ 4 files changed, 151 insertions(+)
+ create mode 100644 m4/init-package-version.m4
+ create mode 100644 modules/package-version
+
+--- /dev/null
++++ b/m4/init-package-version.m4
+@@ -0,0 +1,124 @@
++# init-package-version.m4
++# serial 3
++dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
++dnl This file is free software, distributed under the terms of the GNU
++dnl General Public License. As a special exception to the GNU General
++dnl Public License, this file may be distributed as part of a program
++dnl that contains a configuration script generated by Autoconf, under
++dnl the same distribution terms as the rest of that program.
++
++# Make it possible to pass version numbers extracted from a file in
++# $(srcdir) to autoconf.
++#
++# Autoconf insists on passing the package name and version number to
++# every generated .h file and every Makefile. This was a reasonable
++# design at times when a version number was changed only once a month.
++# Nowadays, people often assign a new version number once a week, or
++# even change it each time a 'git' commit is made. Regenerating all
++# the files that depend on configure.ac (aclocal.m4, configure,
++# config.status, config.h, all Makefiles) may take 15 minutes. These
++# delays can severely hamper development.
++#
++# An alternative is to store the version number in a file in $(srcdir)
++# that is separate from configure.ac. It can be a data file, a shell
++# script, a .m4 file, or other. The essential point is that the maintainer
++# is responsible for creating Makefile dependencies to this version file
++# for every file that needs to be rebuilt when the version changes. This
++# typically includes
++# - distributable documentation files that carry the version number,
++# but does not include
++# - aclocal.m4, configure, config.status, config.h, all Makefiles,
++# - executables.
++#
++# autoconf and automake make it hard to follow this approach:
++#
++# - If AC_INIT is used with arguments, there is a chicken-and-egg problem:
++# The arguments need to be read from a file in $(srcdir). The location
++# of $(srcdir) is only determined by AC_CONFIG_SRCDIR. AC_CONFIG_SRCDIR
++# can only appear after AC_INIT (otherwise aclocal gives an error:
++# "error: m4_defn: undefined macro: _m4_divert_diversion").
++# Furthermore, the arguments passed to AC_INIT must be literals; for
++# example, the assignment to PACKAGE_VERSION looks like this:
++# [PACKAGE_VERSION=']AC_PACKAGE_VERSION[']
++#
++# - If AC_INIT is used without arguments:
++# Automake provides its own variables, PACKAGE and VERSION, and uses them
++# instead of PACKAGE_NAME and PACKAGE_VERSION that come from Autoconf.
++# - If AM_INIT_AUTOMAKE is used with two arguments, automake options
++# like 'silent-rules' cannot be specified.
++# - If AM_INIT_AUTOMAKE is used in its one-argument form or without
++# arguments at all, it triggers an error
++# "error: AC_INIT should be called with package and version arguments".
++# - If AM_INIT_AUTOMAKE is used in its one-argument form or without
++# arguments at all, and _AC_INIT_PACKAGE is used before it, with
++# the package and version number from the file as arguments, we get
++# a warning: "warning: AC_INIT: not a literal: $VERSION_NUMBER".
++# The arguments passed to _AC_INIT_PACKAGE must be literals.
++#
++# With the macro defined in this file, the approach can be coded like this:
++#
++# AC_INIT
++# AC_CONFIG_SRCDIR(WITNESS)
++# . $srcdir/../version.sh
++# gl_INIT_PACKAGE(PACKAGE, $VERSION_NUMBER)
++# AM_INIT_AUTOMAKE([OPTIONS])
++#
++# and after changing version.sh, the developer can directly configure and build:
++#
++# make distclean
++# ./configure
++# make
++#
++# Some other packages use another approach:
++#
++# AC_INIT(PACKAGE,
++# m4_normalize(m4_esyscmd([. ./version.sh; echo $VERSION_NUMBER])))
++# AC_CONFIG_SRCDIR(WITNESS)
++# AM_INIT_AUTOMAKE([OPTIONS])
++#
++# but here, after changing version.sh, the developer must first regenerate the
++# configure file:
++#
++# make distclean
++# ./autogen.sh --skip-gnulib
++# ./configure
++# make
++#
++
++# gl_INIT_PACKAGE(PACKAGE-NAME, VERSION)
++# --------------------------------------
++# followed by an AM_INIT_AUTOMAKE invocation,
++# is like calling AM_INIT_AUTOMAKE(PACKAGE-NAME, VERSION)
++# except that it can use computed non-literal arguments.
++AC_DEFUN([gl_INIT_PACKAGE],
++[
++ AC_BEFORE([$0], [AM_INIT_AUTOMAKE])
++ dnl Redefine AM_INIT_AUTOMAKE.
++ m4_define([gl_AM_INIT_AUTOMAKE],
++ m4_bpatsubst(m4_dquote(
++ m4_bpatsubst(m4_dquote(
++ m4_bpatsubst(m4_dquote(
++ m4_defn([AM_INIT_AUTOMAKE])),
++ [AC_PACKAGE_NAME], [gl_INIT_DUMMY])),
++ [AC_PACKAGE_TARNAME], [gl_INIT_EMPTY])),
++ [AC_PACKAGE_VERSION], [gl_INIT_DUMMY])
++ [AC_SUBST([PACKAGE], [$1])
++ AC_SUBST([VERSION], [$2])
++ ])
++ m4_define([AM_INIT_AUTOMAKE],
++ m4_defn([gl_RPL_INIT_AUTOMAKE]))
++])
++m4_define([gl_INIT_EMPTY], [])
++dnl Automake 1.16.4 no longer accepts an empty value for gl_INIT_DUMMY.
++dnl But a macro that later expands to empty works.
++m4_define([gl_INIT_DUMMY], [gl_INIT_DUMMY2])
++m4_define([gl_INIT_DUMMY2], [])
++AC_DEFUN([gl_RPL_INIT_AUTOMAKE], [
++ m4_ifval([$2],
++ [m4_fatal([After gl_INIT_PACKAGE, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
++ gl_AM_INIT_AUTOMAKE([$1 no-define])
++ m4_if(m4_index([ $1 ], [ no-define ]), [-1],
++ [AC_DEFINE_UNQUOTED(PACKAGE, "$PACKAGE", [Name of package])
++ AC_DEFINE_UNQUOTED(VERSION, "$VERSION", [Version number of package])
++ ])
++])
+--- a/modules/git-version-gen
++++ b/modules/git-version-gen
+@@ -5,6 +5,7 @@ Files:
+ build-aux/git-version-gen
+
+ Depends-on:
++package-version
+
+ configure.ac:
+
+--- /dev/null
++++ b/modules/package-version
+@@ -0,0 +1,19 @@
++Description:
++Support for a computed version string.
++
++Files:
++m4/init-package-version.m4
++
++Depends-on:
++
++configure.ac:
++
++Makefile.am:
++
++Include:
++
++License:
++GPLed build tool
++
++Maintainer:
++Bruno Haible
--- /dev/null
+From bb0f82be83d43db9cd77049be32ffd0b92ab5bb7 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Fri, 24 Jan 2025 22:03:29 +0100
+Subject: package-version: Simplify its use.
+
+Reported by Basil L. Contovounesios <basil@contovou.net> in
+<https://lists.gnu.org/archive/html/bug-gnulib/2025-01/msg00195.html>.
+
+* doc/package-version.texi (Propagating the package version): Recommend
+to pass the usual arguments to AC_INIT.
+* m4/init-package-version.m4: Likewise.
+(gl_INIT_PACKAGE): Define PACKAGE_VERSION and PACKAGE_STRING as needed.
+(gl_RPL_INIT_AUTOMAKE): Improve quoting.
+---
+ ChangeLog | 11 +++++++++++
+ doc/package-version.texi | 2 +-
+ m4/init-package-version.m4 | 20 ++++++++++++++------
+ 3 files changed, 26 insertions(+), 7 deletions(-)
+
+--- a/m4/init-package-version.m4
++++ b/m4/init-package-version.m4
+@@ -1,5 +1,5 @@
+ # init-package-version.m4
+-# serial 3
++# serial 4
+ dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
+ dnl This file is free software, distributed under the terms of the GNU
+ dnl General Public License. As a special exception to the GNU General
+@@ -57,7 +57,7 @@ dnl the same distribution terms as the r
+ #
+ # With the macro defined in this file, the approach can be coded like this:
+ #
+-# AC_INIT
++# AC_INIT(PACKAGE, [dummy], [MORE OPTIONS])
+ # AC_CONFIG_SRCDIR(WITNESS)
+ # . $srcdir/../version.sh
+ # gl_INIT_PACKAGE(PACKAGE, $VERSION_NUMBER)
+@@ -102,8 +102,16 @@ AC_DEFUN([gl_INIT_PACKAGE],
+ [AC_PACKAGE_NAME], [gl_INIT_DUMMY])),
+ [AC_PACKAGE_TARNAME], [gl_INIT_EMPTY])),
+ [AC_PACKAGE_VERSION], [gl_INIT_DUMMY])
+- [AC_SUBST([PACKAGE], [$1])
+- AC_SUBST([VERSION], [$2])
++ [dnl Set variables documented in Automake.
++ AC_SUBST([PACKAGE], [$1])
++ AC_SUBST([VERSION], ["$2"])
++ dnl Set variables documented in Autoconf.
++ AC_SUBST([PACKAGE_VERSION], ["$2"])
++ AC_SUBST([PACKAGE_STRING], ["$1 $2"])
++ AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$2"],
++ [Define to the version of this package.])
++ AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["$1 $2"],
++ [Define to the full name and version of this package.])
+ ])
+ m4_define([AM_INIT_AUTOMAKE],
+ m4_defn([gl_RPL_INIT_AUTOMAKE]))
+@@ -118,7 +126,7 @@ AC_DEFUN([gl_RPL_INIT_AUTOMAKE], [
+ [m4_fatal([After gl_INIT_PACKAGE, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
+ gl_AM_INIT_AUTOMAKE([$1 no-define])
+ m4_if(m4_index([ $1 ], [ no-define ]), [-1],
+- [AC_DEFINE_UNQUOTED(PACKAGE, "$PACKAGE", [Name of package])
+- AC_DEFINE_UNQUOTED(VERSION, "$VERSION", [Version number of package])
++ [AC_DEFINE_UNQUOTED([PACKAGE], ["$PACKAGE"], [Name of package])
++ AC_DEFINE_UNQUOTED([VERSION], ["$VERSION"], [Version number of package])
+ ])
+ ])
--- /dev/null
+From 48648b4b9b3fd79a5c68913deb28678bd9d8eb34 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Sat, 25 Jan 2025 04:07:32 +0100
+Subject: package-version: Simplify further.
+
+* doc/package-version.texi (Propagating the package version): Recommend
+use of gl_INIT_PACKAGE_VERSION instead of gl_INIT_PACKAGE.
+* build-aux/git-version-gen: Likewise.
+* m4/init-package-version.m4: Likewise.
+(gl_INIT_PACKAGE_VERSION): Renamed from gl_INIT_PACKAGE. Take only one
+argument. Don't fiddle with AC_PACKAGE_NAME, AC_PACKAGE_TARNAME,
+PACKAGE.
+(gl_RPL_INIT_AUTOMAKE): Update.
+---
+ ChangeLog | 10 ++++++++++
+ build-aux/git-version-gen | 4 ++--
+ doc/package-version.texi | 4 ++--
+ m4/init-package-version.m4 | 30 ++++++++++++------------------
+ 4 files changed, 26 insertions(+), 22 deletions(-)
+
+--- a/m4/init-package-version.m4
++++ b/m4/init-package-version.m4
+@@ -1,5 +1,5 @@
+ # init-package-version.m4
+-# serial 4
++# serial 5
+ dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
+ dnl This file is free software, distributed under the terms of the GNU
+ dnl General Public License. As a special exception to the GNU General
+@@ -60,7 +60,7 @@ dnl the same distribution terms as the r
+ # AC_INIT(PACKAGE, [dummy], [MORE OPTIONS])
+ # AC_CONFIG_SRCDIR(WITNESS)
+ # . $srcdir/../version.sh
+-# gl_INIT_PACKAGE(PACKAGE, $VERSION_NUMBER)
++# gl_INIT_PACKAGE_VERSION($VERSION_NUMBER)
+ # AM_INIT_AUTOMAKE([OPTIONS])
+ #
+ # and after changing version.sh, the developer can directly configure and build:
+@@ -85,32 +85,26 @@ dnl the same distribution terms as the r
+ # make
+ #
+
+-# gl_INIT_PACKAGE(PACKAGE-NAME, VERSION)
+-# --------------------------------------
++# gl_INIT_PACKAGE_VERSION(VERSION)
++# --------------------------------
+ # followed by an AM_INIT_AUTOMAKE invocation,
+ # is like calling AM_INIT_AUTOMAKE(PACKAGE-NAME, VERSION)
+ # except that it can use computed non-literal arguments.
+-AC_DEFUN([gl_INIT_PACKAGE],
++AC_DEFUN([gl_INIT_PACKAGE_VERSION],
+ [
+ AC_BEFORE([$0], [AM_INIT_AUTOMAKE])
+ dnl Redefine AM_INIT_AUTOMAKE.
+ m4_define([gl_AM_INIT_AUTOMAKE],
+- m4_bpatsubst(m4_dquote(
+- m4_bpatsubst(m4_dquote(
+- m4_bpatsubst(m4_dquote(
+- m4_defn([AM_INIT_AUTOMAKE])),
+- [AC_PACKAGE_NAME], [gl_INIT_DUMMY])),
+- [AC_PACKAGE_TARNAME], [gl_INIT_EMPTY])),
++ m4_bpatsubst(m4_dquote(m4_defn([AM_INIT_AUTOMAKE])),
+ [AC_PACKAGE_VERSION], [gl_INIT_DUMMY])
+ [dnl Set variables documented in Automake.
+- AC_SUBST([PACKAGE], [$1])
+- AC_SUBST([VERSION], ["$2"])
++ AC_SUBST([VERSION], ["$1"])
+ dnl Set variables documented in Autoconf.
+- AC_SUBST([PACKAGE_VERSION], ["$2"])
+- AC_SUBST([PACKAGE_STRING], ["$1 $2"])
+- AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$2"],
++ AC_SUBST([PACKAGE_VERSION], ["$1"])
++ AC_SUBST([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"])
++ AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$1"],
+ [Define to the version of this package.])
+- AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["$1 $2"],
++ AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"],
+ [Define to the full name and version of this package.])
+ ])
+ m4_define([AM_INIT_AUTOMAKE],
+@@ -123,7 +117,7 @@ m4_define([gl_INIT_DUMMY], [gl_INIT_DUMM
+ m4_define([gl_INIT_DUMMY2], [])
+ AC_DEFUN([gl_RPL_INIT_AUTOMAKE], [
+ m4_ifval([$2],
+- [m4_fatal([After gl_INIT_PACKAGE, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
++ [m4_fatal([After gl_INIT_PACKAGE_VERSION, the two-argument form of AM_INIT_AUTOMAKE cannot be used.])])
+ gl_AM_INIT_AUTOMAKE([$1 no-define])
+ m4_if(m4_index([ $1 ], [ no-define ]), [-1],
+ [AC_DEFINE_UNQUOTED([PACKAGE], ["$PACKAGE"], [Name of package])
--- /dev/null
+From 2e46209809f751087ca27523283bd5c3e9071d31 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Sun, 26 Jan 2025 13:26:35 +0100
+Subject: package-version: Avoid compiler warnings in config.log.
+
+* m4/init-package-version.m4 (gl_INIT_PACKAGE_VERSION): Undefine
+PACKAGE_VERSION and PACKAGE_STRING before redefining them.
+---
+ ChangeLog | 6 ++++++
+ m4/init-package-version.m4 | 4 +++-
+ 2 files changed, 9 insertions(+), 1 deletion(-)
+
+--- a/m4/init-package-version.m4
++++ b/m4/init-package-version.m4
+@@ -1,5 +1,5 @@
+ # init-package-version.m4
+-# serial 5
++# serial 6
+ dnl Copyright (C) 1992-2025 Free Software Foundation, Inc.
+ dnl This file is free software, distributed under the terms of the GNU
+ dnl General Public License. As a special exception to the GNU General
+@@ -102,8 +102,10 @@ AC_DEFUN([gl_INIT_PACKAGE_VERSION],
+ dnl Set variables documented in Autoconf.
+ AC_SUBST([PACKAGE_VERSION], ["$1"])
+ AC_SUBST([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"])
++ _AC_DEFINE([#undef PACKAGE_VERSION])
+ AC_DEFINE_UNQUOTED([PACKAGE_VERSION], ["$1"],
+ [Define to the version of this package.])
++ _AC_DEFINE([#undef PACKAGE_STRING])
+ AC_DEFINE_UNQUOTED([PACKAGE_STRING], ["AC_PACKAGE_NAME $1"],
+ [Define to the full name and version of this package.])
+ ])
--- /dev/null
+From 85599643e2fbf70f7f0bd58831993132ef335705 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Wed, 22 Jan 2025 21:25:27 +0100
+Subject: New module 'version-stamp'.
+
+* m4/version-stamp.m4: New file.
+* modules/version-stamp: New file.
+---
+ ChangeLog | 6 ++++++
+ m4/version-stamp.m4 | 35 +++++++++++++++++++++++++++++++++++
+ modules/version-stamp | 19 +++++++++++++++++++
+ 3 files changed, 60 insertions(+)
+ create mode 100644 m4/version-stamp.m4
+ create mode 100644 modules/version-stamp
+
+--- /dev/null
++++ b/m4/version-stamp.m4
+@@ -0,0 +1,35 @@
++# version-stamp.m4
++# serial 1
++dnl Copyright (C) 2025 Free Software Foundation, Inc.
++dnl This file is free software, distributed under the terms of the GNU
++dnl General Public License. As a special exception to the GNU General
++dnl Public License, this file may be distributed as part of a program
++dnl that contains a configuration script generated by Autoconf, under
++dnl the same distribution terms as the rest of that program.
++
++# Manages a stamp file, that keeps track when $(VERSION) was last changed.
++#
++# gl_CONFIG_VERSION_STAMP
++# needs to be invoked near the end of the package's top-level configure.ac,
++# before AC_OUTPUT.
++# It makes sure that during the build,
++# - $(top_srcdir)/.version exists, and
++# - when $(VERSION) is changed, $(top_srcdir)/.version gets modified.
++#
++# $(top_srcdir)/.version is a stamp file. Its contents wouldn't matter,
++# except that for detecting the change, we store the value of $(VERSION)
++# in it (but we could just as well store it in a different file).
++AC_DEFUN([gl_CONFIG_VERSION_STAMP],
++[
++ AC_CONFIG_COMMANDS([version-timestamp],
++ [if test -f "$ac_top_srcdir/.version" \
++ && test `cat "$ac_top_srcdir/.version"` = "$gl_version"; then
++ # The value of $(VERSION) is the same as last time.
++ :
++ else
++ # The value of $(VERSION) has changed. Update the stamp.
++ echo "$gl_version" > "$ac_top_srcdir/.version"
++ fi
++ ],
++ [gl_version="$VERSION"])
++])
+--- /dev/null
++++ b/modules/version-stamp
+@@ -0,0 +1,19 @@
++Description:
++Optimized rebuilding of artifacts that depend on $(VERSION).
++
++Files:
++m4/version-stamp.m4
++
++Depends-on:
++
++configure.ac:
++
++Makefile.am:
++
++Include:
++
++License:
++GPLed build tool
++
++Maintainer:
++Bruno Haible
--- /dev/null
+From 701d20aaf579bb71f35209dd63a272c3d9d21096 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Mon, 24 Feb 2025 19:03:17 +0100
+Subject: [PATCH] vc-mtime: New module.
+
+* lib/vc-mtime.h: New file.
+* lib/vc-mtime.c: New file.
+* modules/vc-mtime: New file.
+---
+ ChangeLog | 7 ++
+ lib/vc-mtime.c | 208 +++++++++++++++++++++++++++++++++++++++++++++++
+ lib/vc-mtime.h | 97 ++++++++++++++++++++++
+ modules/vc-mtime | 34 ++++++++
+ 4 files changed, 346 insertions(+)
+ create mode 100644 lib/vc-mtime.c
+ create mode 100644 lib/vc-mtime.h
+ create mode 100644 modules/vc-mtime
+
+--- /dev/null
++++ b/lib/vc-mtime.c
+@@ -0,0 +1,208 @@
++/* Return the version-control based modification time of a file.
++ Copyright (C) 2025 Free Software Foundation, Inc.
++
++ This program is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ This program 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 General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with this program. If not, see <https://www.gnu.org/licenses/>. */
++
++/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
++
++#include <config.h>
++
++/* Specification. */
++#include "vc-mtime.h"
++
++#include <stdlib.h>
++#include <unistd.h>
++
++#include <error.h>
++#include "spawn-pipe.h"
++#include "wait-process.h"
++#include "execute.h"
++#include "safe-read.h"
++#include "xstrtol.h"
++#include "stat-time.h"
++#include "gettext.h"
++
++#define _(msgid) dgettext ("gnulib", msgid)
++
++
++/* Determines whether the specified file is under version control. */
++static bool
++git_vc_controlled (const char *filename)
++{
++ /* Run "git ls-files FILENAME" and return true if the exit code is 0
++ and the output is non-empty. */
++ const char *argv[4];
++ pid_t child;
++ int fd[1];
++
++ argv[0] = "git";
++ argv[1] = "ls-files";
++ argv[2] = filename;
++ argv[3] = NULL;
++ child = create_pipe_in ("git", "git", argv, NULL, NULL,
++ DEV_NULL, true, true, false, fd);
++ if (child == -1)
++ return false;
++
++ /* Read the subprocess output, and test whether it is non-empty. */
++ size_t count = 0;
++ char c;
++
++ while (safe_read (fd[0], &c, 1) > 0)
++ count++;
++
++ close (fd[0]);
++
++ /* Remove zombie process from process list, and retrieve exit status. */
++ int exitstatus =
++ wait_subprocess (child, "git", false, true, true, false, NULL);
++ return (exitstatus == 0 && count > 0);
++}
++
++/* Determines whether the specified file is unmodified, compared to the
++ last version in version control. */
++static bool
++git_unmodified (const char *filename)
++{
++ /* Run "git diff --quiet -- HEAD FILENAME"
++ (or "git diff --quiet HEAD FILENAME")
++ and return true if the exit code is 0.
++ The '--' option is for the case that the specified file was removed. */
++ const char *argv[7];
++ int exitstatus;
++
++ argv[0] = "git";
++ argv[1] = "diff";
++ argv[2] = "--quiet";
++ argv[3] = "--";
++ argv[4] = "HEAD";
++ argv[5] = filename;
++ argv[6] = NULL;
++ exitstatus = execute ("git", "git", argv, NULL, NULL,
++ false, false, true, true,
++ true, false, NULL);
++ return (exitstatus == 0);
++}
++
++/* Stores in *MTIME the time of last modification in version control of the
++ specified file, and returns 0.
++ Upon failure, it returns -1. */
++static int
++git_mtime (struct timespec *mtime, const char *filename)
++{
++ /* Run "git log -1 --format=%ct -- FILENAME". It prints the time of last
++ modification, as the number of seconds since the Epoch.
++ The '--' option is for the case that the specified file was removed. */
++ const char *argv[7];
++ pid_t child;
++ int fd[1];
++
++ argv[0] = "git";
++ argv[1] = "log";
++ argv[2] = "-1";
++ argv[3] = "--format=%ct";
++ argv[4] = "--";
++ argv[5] = filename;
++ argv[6] = NULL;
++ child = create_pipe_in ("git", "git", argv, NULL, NULL,
++ DEV_NULL, true, true, false, fd);
++ if (child == -1)
++ return -1;
++
++ /* Retrieve its result. */
++ FILE *fp;
++ char *line;
++ size_t linesize;
++ size_t linelen;
++
++ fp = fdopen (fd[0], "r");
++ if (fp == NULL)
++ error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++ line = NULL; linesize = 0;
++ linelen = getline (&line, &linesize, fp);
++ if (linelen == (size_t)(-1))
++ {
++ error (0, 0, _("%s subprocess I/O error"), "git");
++ fclose (fp);
++ wait_subprocess (child, "git", true, false, true, false, NULL);
++ }
++ else
++ {
++ int exitstatus;
++
++ if (linelen > 0 && line[linelen - 1] == '\n')
++ line[linelen - 1] = '\0';
++
++ fclose (fp);
++
++ /* Remove zombie process from process list, and retrieve exit status. */
++ exitstatus =
++ wait_subprocess (child, "git", true, false, true, false, NULL);
++ if (exitstatus == 0)
++ {
++ char *endptr;
++ unsigned long git_log_time;
++ if (xstrtoul (line, &endptr, 10, &git_log_time, NULL) == LONGINT_OK
++ && endptr == line + strlen (line))
++ {
++ mtime->tv_sec = git_log_time;
++ mtime->tv_nsec = 0;
++ free (line);
++ return 0;
++ }
++ }
++ }
++ free (line);
++ return -1;
++}
++
++int
++vc_mtime (struct timespec *mtime, const char *filename)
++{
++ static bool git_tested;
++ static bool git_present;
++
++ if (!git_tested)
++ {
++ /* Test for presence of git:
++ "git --version >/dev/null 2>/dev/null" */
++ const char *argv[3];
++ int exitstatus;
++
++ argv[0] = "git";
++ argv[1] = "--version";
++ argv[2] = NULL;
++ exitstatus = execute ("git", "git", argv, NULL, NULL,
++ false, false, true, true,
++ true, false, NULL);
++ git_present = (exitstatus == 0);
++ git_tested = true;
++ }
++
++ if (git_present
++ && git_vc_controlled (filename)
++ && git_unmodified (filename))
++ {
++ if (git_mtime (mtime, filename) == 0)
++ return 0;
++ }
++ struct stat statbuf;
++ if (stat (filename, &statbuf) == 0)
++ {
++ *mtime = get_stat_mtime (&statbuf);
++ return 0;
++ }
++ return -1;
++}
+--- /dev/null
++++ b/lib/vc-mtime.h
+@@ -0,0 +1,97 @@
++/* Return the version-control based modification time of a file.
++ Copyright (C) 2025 Free Software Foundation, Inc.
++
++ This program is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ This program 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 General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with this program. If not, see <https://www.gnu.org/licenses/>. */
++
++/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
++
++#ifndef _VC_MTIME_H
++#define _VC_MTIME_H
++
++/* Get struct timespec. */
++#include <time.h>
++
++/* The "version-controlled modification time" vc_mtime(F) of a file F
++ is defined as:
++ - If F is under version control and not modified locally:
++ the time of the last change of F in the version control system.
++ - Otherwise: The modification time of F on disk.
++
++ For now, the only VCS supported by this module is git. (hg and svn are
++ hardly in use any more.)
++
++ This has the properties that:
++ - Different users who have checked out the same git repo on different
++ machines, at different times, and not done local modifications,
++ get the same vc_mtime(F).
++ - If a user has modified F locally, the modification time of that file
++ counts.
++ - If that user then reverts the modification, they then again get the
++ same vc_mtime(F) as everyone else.
++ - Different users who have unpacked the same tarball (without .git
++ directory) on different machines, at different times, also get the same
++ vc_mtime(F) [but possibly a different one than when the .git directory
++ was present]. (Assuming a POSIX compliant file system.)
++ - When a user commits local modifications into git, this only increases
++ (not decreases) the vc_mtime(F).
++
++ The purpose of the version-controlled modification time is to produce a
++ reproducible timestamp(Z) of a file Z that depends on files X1, ..., Xn,
++ in such a way that
++ - timestamp(Z) is reproducible, that is, different users on different
++ machines get the same value.
++ - timestamp(Z) is related to reality. It's not just a dummy, like what
++ is suggested in <https://reproducible-builds.org/docs/timestamps/>.
++ - One can arrange for timestamp(Z) to respect the modification time
++ relations of a build system.
++
++ There are two uses of such a timestamp:
++ - It can be set as the modification time of file Z in a file system, or
++ - It can be embedded in Z, with the purpose of telling a user how old
++ the file Z is. For example, in PDF files or in generated documentation,
++ such a time is embedded in a special place.
++
++ The simplest example is a file Z that depends on files X1, ..., Xn.
++ Generally one will define
++ timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn))
++ for an embedded timestamp, or
++ timestamp(Z) = max (vc_mtime(X1), ..., vc_mtime(Xn)) + 1 second
++ for a time stamp in a file system. The added second
++ 1. accounts for fractional seconds in mtime(X1), ..., mtime(Xn),
++ 2. allows for 'make' implementation that attempt to rebuild Z
++ if mtime(Z) == mtime(Xi).
++
++ A more complicated example is when there are intermediate built files, not
++ under version control. For example, if the build process produces
++ X1, X2 -> Y1
++ X3, X4 -> Y2
++ Y1, Y2, X5 -> Z
++ where Y1 and Y2 are intermediate built files, you should ignore the
++ mtime(Y1), mtime(Y2), and consider only the vc_mtime(X1), ..., vc_mtime(X5).
++ */
++
++#ifdef __cplusplus
++extern "C" {
++#endif
++
++/* Determines the version-controlled modification time of FILENAME, stores it
++ in *MTIME, and returns 0.
++ Upon failure, it returns -1. */
++extern int vc_mtime (struct timespec *mtime, const char *filename);
++
++#ifdef __cplusplus
++}
++#endif
++
++#endif /* _VC_MTIME_H */
+--- /dev/null
++++ b/modules/vc-mtime
+@@ -0,0 +1,34 @@
++Description:
++Returns the version-control based modification time of a file.
++
++Files:
++lib/vc-mtime.h
++lib/vc-mtime.c
++
++Depends-on:
++time-h
++bool
++spawn-pipe
++wait-process
++execute
++safe-read
++error
++getline
++xstrtol
++stat-time
++gettext-h
++gnulib-i18n
++
++configure.ac:
++
++Makefile.am:
++lib_SOURCES += vc-mtime.c
++
++Include:
++"vm-mtime.h"
++
++License:
++GPL
++
++Maintainer:
++Bruno Haible
--- /dev/null
+From f47c5f2e21d0ccedb271b406e35b6963b23a64c4 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Wed, 30 Apr 2025 13:11:01 +0200
+Subject: [PATCH] clean-temp: Fix link error (regression yesterday).
+
+* lib/clean-temp.c: Include hashkey-string.h.
+(create_temp_dir): Use hashkey_string_* functions instead of
+clean_temp_string_*.
+* lib/clean-temp-private.h (clean_temp_string_equals,
+clean_temp_string_hash): Remove declarations.
+* modules/clean-temp (Depends-on): Add hashkey-string.
+---
+ ChangeLog | 10 ++++++++++
+ lib/clean-temp-private.h | 3 ---
+ lib/clean-temp.c | 5 +++--
+ modules/clean-temp | 1 +
+ 4 files changed, 14 insertions(+), 5 deletions(-)
+
+--- a/lib/clean-temp-private.h
++++ b/lib/clean-temp-private.h
+@@ -68,9 +68,6 @@ struct closeable_fd
+ #define descriptors clean_temp_descriptors
+ extern gl_list_t /* <closeable_fd *> */ volatile descriptors;
+
+-extern bool clean_temp_string_equals (const void *x1, const void *x2);
+-extern size_t clean_temp_string_hash (const void *x);
+-
+ extern _GL_ASYNC_SAFE int clean_temp_asyncsafe_close (struct closeable_fd *element);
+ extern void clean_temp_init_asyncsafe_close (void);
+
+--- a/lib/clean-temp.c
++++ b/lib/clean-temp.c
+@@ -45,6 +45,7 @@
+ #include "xmalloca.h"
+ #include "glthread/lock.h"
+ #include "thread-optim.h"
++#include "hashkey-string.h"
+ #include "gl_xlist.h"
+ #include "gl_linkedhash_list.h"
+ #include "gl_linked_list.h"
+@@ -221,11 +222,11 @@ create_temp_dir (const char *prefix, con
+ tmpdir->cleanup_verbose = cleanup_verbose;
+ tmpdir->subdirs =
+ gl_list_create_empty (GL_LINKEDHASH_LIST,
+- clean_temp_string_equals, clean_temp_string_hash,
++ hashkey_string_equals, hashkey_string_hash,
+ NULL, false);
+ tmpdir->files =
+ gl_list_create_empty (GL_LINKEDHASH_LIST,
+- clean_temp_string_equals, clean_temp_string_hash,
++ hashkey_string_equals, hashkey_string_hash,
+ NULL, false);
+
+ /* Create the temporary directory. */
+--- a/modules/clean-temp
++++ b/modules/clean-temp
+@@ -24,6 +24,7 @@ rmdir
+ xalloc
+ xalloc-die
+ xmalloca
++hashkey-string
+ linkedhash-list
+ linked-list
+ xlist
--- /dev/null
+From 9a26e7043fa95b4c9ee4576ce8c0ac15668e695e Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Thu, 2 Jan 2025 13:54:54 +0100
+Subject: [PATCH] string-desc, xstring-desc, string-desc-quotearg: Rename
+ functions.
+
+* lib/string-desc.h (sd_equals): Renamed from string_desc_equals.
+(sd_startswith): Renamed from string_desc_startswith.
+(sd_endswith): Renamed from string_desc_endswith.
+(sd_cmp): Renamed from string_desc_cmp.
+(sd_c_casecmp): Renamed from string_desc_c_casecmp.
+(sd_index): Renamed from string_desc_index.
+(sd_last_index): Renamed from string_desc_last_index.
+(sd_contains): Renamed from string_desc_contains.
+(sd_new_empty): Renamed from string_desc_new_empty.
+(sd_new_addr): Renamed from string_desc_new_addr.
+(sd_from_c): Renamed from string_desc_from_c.
+(sd_substring): Renamed from string_desc_substring.
+(sd_write): Renamed from string_desc_write.
+(sd_fwrite): Renamed from string_desc_fwrite.
+(sd_new): Renamed from string_desc_new.
+(sd_new_filled): Renamed from string_desc_new_filled.
+(sd_copy): Renamed from string_desc_copy.
+(sd_concat): Renamed from string_desc_concat.
+(sd_c): Renamed from string_desc_c.
+(sd_set_char_at): Renamed from string_desc_set_char_at.
+(sd_fill): Renamed from string_desc_fill.
+(sd_overwrite): Renamed from string_desc_overwrite.
+(sd_free): Renamed from string_desc_free.
+(sd_length): Renamed from string_desc_length.
+(sd_char_at): Renamed from string_desc_char_at.
+(sd_data): Renamed from string_desc_data.
+(sd_is_empty): Renamed from string_desc_is_empty.
+* lib/string-desc.c (sd_equals): Renamed from string_desc_equals.
+(sd_startswith): Renamed from string_desc_startswith.
+(sd_endswith): Renamed from string_desc_endswith.
+(sd_cmp): Renamed from string_desc_cmp.
+(sd_c_casecmp): Renamed from string_desc_c_casecmp.
+(sd_index): Renamed from string_desc_index.
+(sd_last_index): Renamed from string_desc_last_index.
+(sd_new_empty): Renamed from string_desc_new_empty.
+(sd_new_addr): Renamed from string_desc_new_addr.
+(sd_from_c): Renamed from string_desc_from_c.
+(sd_substring): Renamed from string_desc_substring.
+(sd_write): Renamed from string_desc_write.
+(sd_fwrite): Renamed from string_desc_fwrite.
+(sd_new): Renamed from string_desc_new.
+(sd_new_filled): Renamed from string_desc_new_filled.
+(sd_copy): Renamed from string_desc_copy.
+(sd_concat): Renamed from string_desc_concat.
+(sd_c): Renamed from string_desc_c.
+(sd_set_char_at): Renamed from string_desc_set_char_at.
+(sd_fill): Renamed from string_desc_fill.
+(sd_overwrite): Renamed from string_desc_overwrite.
+(sd_free): Renamed from string_desc_free.
+* lib/xstring-desc.h (xsd_concat): Renamed from xstring_desc_concat.
+(xsd_new): Renamed from xstring_desc_new.
+(xsd_new_filled): Renamed from xstring_desc_new_filled.
+(xsd_copy): Renamed from xstring_desc_copy.
+(xsd_c): Renamed from xstring_desc_c.
+* lib/xstring-desc.c (xsd_concat): Renamed from xstring_desc_concat.
+* lib/string-desc-quotearg.h (sd_quotearg_buffer): Renamed from
+string_desc_quotearg_buffer.
+(sd_quotearg_alloc): Renamed from string_desc_quotearg_alloc.
+(sd_quotearg_n): Renamed from string_desc_quotearg_n.
+(sd_quotearg): Renamed from string_desc_quotearg.
+(sd_quotearg_n_style): Renamed from string_desc_quotearg_n_style.
+(sd_quotearg_style): Renamed from string_desc_quotearg_style.
+(sd_quotearg_char): Renamed from string_desc_quotearg_char.
+(sd_quotearg_colon): Renamed from string_desc_quotearg_colon.
+(sd_quotearg_n_custom): Renamed from string_desc_quotearg_n_custom.
+(sd_quotearg_custom): Renamed from sd_quotearg_n_custom.
+* lib/string-desc-contains.c (sd_contains): Renamed from
+string_desc_contains.
+* lib/string-buffer.h: Update.
+* lib/string-buffer.c (sb_append_desc, sb_contents, sb_dupfree): Update.
+* lib/xstring-buffer.c (sb_xdupfree): Update.
+* lib/sf-istream.c (sf_istream_init_from_string_desc): Update.
+* tests/test-string-desc.c (main): Update.
+* tests/test-string-desc.sh: Update.
+* tests/test-xstring-desc.c (main): Update.
+* tests/test-string-desc-quotearg.c (main): Update.
+* tests/test-string-buffer.c (main): Update.
+* tests/test-sf-istream.c (main): Update.
+* tests/test-sfl-istream.c (main): Update.
+* doc/string-desc.texi: Update.
+* doc/strings.texi: Update.
+* NEWS: Mention the change.
+---
+ ChangeLog | 86 +++++++++
+ NEWS | 4 +
+ doc/string-desc.texi | 4 +-
+ doc/strings.texi | 18 +-
+ lib/sf-istream.c | 4 +-
+ lib/string-buffer.c | 12 +-
+ lib/string-buffer.h | 4 +-
+ lib/string-desc-contains.c | 2 +-
+ lib/string-desc-quotearg.h | 114 ++++++------
+ lib/string-desc.c | 52 +++---
+ lib/string-desc.h | 62 +++----
+ lib/xstring-buffer.c | 4 +-
+ lib/xstring-desc.c | 2 +-
+ lib/xstring-desc.h | 26 +--
+ tests/test-sf-istream.c | 4 +-
+ tests/test-sfl-istream.c | 4 +-
+ tests/test-string-buffer.c | 18 +-
+ tests/test-string-desc-quotearg.c | 44 ++---
+ tests/test-string-desc.c | 296 +++++++++++++++---------------
+ tests/test-string-desc.sh | 4 +-
+ tests/test-xstring-desc.c | 68 +++----
+ 21 files changed, 461 insertions(+), 371 deletions(-)
+
+--- a/lib/sf-istream.c
++++ b/lib/sf-istream.c
+@@ -46,8 +46,8 @@ sf_istream_init_from_string_desc (sf_ist
+ string_desc_t input)
+ {
+ stream->fp = NULL;
+- stream->input = string_desc_data (input);
+- stream->input_end = stream->input + string_desc_length (input);
++ stream->input = sd_data (input);
++ stream->input_end = stream->input + sd_length (input);
+ }
+
+ int
+--- a/lib/string-buffer.c
++++ b/lib/string-buffer.c
+@@ -101,13 +101,13 @@ sb_append1 (struct string_buffer *buffer
+ int
+ sb_append_desc (struct string_buffer *buffer, string_desc_t s)
+ {
+- size_t len = string_desc_length (s);
++ size_t len = sd_length (s);
+ if (sb_ensure_more_bytes (buffer, len) < 0)
+ {
+ buffer->error = true;
+ return -1;
+ }
+- memcpy (buffer->data + buffer->length, string_desc_data (s), len);
++ memcpy (buffer->data + buffer->length, sd_data (s), len);
+ buffer->length += len;
+ return 0;
+ }
+@@ -136,7 +136,7 @@ sb_free (struct string_buffer *buffer)
+ string_desc_t
+ sb_contents (struct string_buffer *buffer)
+ {
+- return string_desc_new_addr (buffer->length, buffer->data);
++ return sd_new_addr (buffer->length, buffer->data);
+ }
+
+ const char *
+@@ -162,7 +162,7 @@ sb_dupfree (struct string_buffer *buffer
+ if (copy == NULL)
+ goto fail;
+ memcpy (copy, buffer->data, length);
+- return string_desc_new_addr (length, copy);
++ return sd_new_addr (length, copy);
+ }
+ else
+ {
+@@ -174,12 +174,12 @@ sb_dupfree (struct string_buffer *buffer
+ if (contents == NULL)
+ goto fail;
+ }
+- return string_desc_new_addr (length, contents);
++ return sd_new_addr (length, contents);
+ }
+
+ fail:
+ sb_free (buffer);
+- return string_desc_new_addr (0, NULL);
++ return sd_new_addr (0, NULL);
+ }
+
+ char *
+--- a/lib/string-buffer.h
++++ b/lib/string-buffer.h
+@@ -115,7 +115,7 @@ extern const char * sb_contents_c (struc
+
+ /* Returns the contents of BUFFER and frees all other memory held by BUFFER.
+ Returns NULL upon failure or if there was an error earlier.
+- It is the responsibility of the caller to string_desc_free() the result. */
++ It is the responsibility of the caller to sd_free() the result. */
+ extern string_desc_t sb_dupfree (struct string_buffer *buffer)
+ _GL_ATTRIBUTE_RELEASE_CAPABILITY (buffer->data);
+
+@@ -182,7 +182,7 @@ extern const char * sb_xcontents_c (stru
+
+ /* Returns the contents of BUFFER and frees all other memory held by BUFFER.
+ Returns (0, NULL) if there was an error earlier.
+- It is the responsibility of the caller to string_desc_free() the result. */
++ It is the responsibility of the caller to sd_free() the result. */
+ extern string_desc_t sb_xdupfree (struct string_buffer *buffer)
+ _GL_ATTRIBUTE_RELEASE_CAPABILITY (buffer->data);
+
+--- a/lib/string-desc-contains.c
++++ b/lib/string-desc-contains.c
+@@ -31,7 +31,7 @@
+ which — depending on platforms — costs up to 2 KB of binary code. */
+
+ ptrdiff_t
+-string_desc_contains (string_desc_t haystack, string_desc_t needle)
++sd_contains (string_desc_t haystack, string_desc_t needle)
+ {
+ if (needle._nbytes == 0)
+ return 0;
+--- a/lib/string-desc-quotearg.h
++++ b/lib/string-desc-quotearg.h
+@@ -50,22 +50,22 @@ extern "C" {
+ does not use backslash escapes and the flags of O do not request
+ elision of null bytes. */
+ #if 0
+-extern size_t string_desc_quotearg_buffer (char *restrict buffer,
+- size_t buffersize,
+- string_desc_t arg,
+- struct quoting_options const *o);
++extern size_t sd_quotearg_buffer (char *restrict buffer,
++ size_t buffersize,
++ string_desc_t arg,
++ struct quoting_options const *o);
+ #endif
+
+-/* Like string_desc_quotearg_buffer, except return the result in a newly
++/* Like sd_quotearg_buffer, except return the result in a newly
+ allocated buffer and store its length, excluding the terminating null
+ byte, in *SIZE. It is the caller's responsibility to free the result.
+ The result might contain embedded null bytes if the style of O does
+ not use backslash escapes and the flags of O do not request elision
+ of null bytes. */
+ #if 0
+-extern char *string_desc_quotearg_alloc (string_desc_t arg,
+- size_t *size,
+- struct quoting_options const *o)
++extern char *sd_quotearg_alloc (string_desc_t arg,
++ size_t *size,
++ struct quoting_options const *o)
+ _GL_ATTRIBUTE_NONNULL ((2))
+ _GL_ATTRIBUTE_MALLOC _GL_ATTRIBUTE_DEALLOC_FREE
+ _GL_ATTRIBUTE_RETURNS_NONNULL;
+@@ -77,68 +77,68 @@ extern char *string_desc_quotearg_alloc
+ reused by the next call to this function with the same value of N.
+ N must be nonnegative. */
+ #if 0
+-extern char *string_desc_quotearg_n (int n, string_desc_t arg);
++extern char *sd_quotearg_n (int n, string_desc_t arg);
+ #endif
+
+-/* Equivalent to string_desc_quotearg_n (0, ARG). */
++/* Equivalent to sd_quotearg_n (0, ARG). */
+ #if 0
+-extern char *string_desc_quotearg (string_desc_t arg);
++extern char *sd_quotearg (string_desc_t arg);
+ #endif
+
+ /* Use style S and storage slot N to return a quoted version of the string ARG.
+- This is like string_desc_quotearg_n (N, ARG), except that it uses S
++ This is like sd_quotearg_n (N, ARG), except that it uses S
+ with no other options to specify the quoting method. */
+ #if 0
+-extern char *string_desc_quotearg_n_style (int n, enum quoting_style s,
+- string_desc_t arg);
++extern char *sd_quotearg_n_style (int n, enum quoting_style s,
++ string_desc_t arg);
+ #endif
+
+-/* Equivalent to string_desc_quotearg_n_style (0, S, ARG). */
++/* Equivalent to sd_quotearg_n_style (0, S, ARG). */
+ #if 0
+-extern char *string_desc_quotearg_style (enum quoting_style s,
+- string_desc_t arg);
++extern char *sd_quotearg_style (enum quoting_style s,
++ string_desc_t arg);
+ #endif
+
+-/* Like string_desc_quotearg (ARG), except also quote any instances of CH.
++/* Like sd_quotearg (ARG), except also quote any instances of CH.
+ See set_char_quoting for a description of acceptable CH values. */
+ #if 0
+-extern char *string_desc_quotearg_char (string_desc_t arg, char ch);
++extern char *sd_quotearg_char (string_desc_t arg, char ch);
+ #endif
+
+-/* Equivalent to string_desc_quotearg_char (ARG, ':'). */
++/* Equivalent to sd_quotearg_char (ARG, ':'). */
+ #if 0
+-extern char *string_desc_quotearg_colon (string_desc_t arg);
++extern char *sd_quotearg_colon (string_desc_t arg);
+ #endif
+
+-/* Like string_desc_quotearg_n_style (N, S, ARG) but with S as
++/* Like sd_quotearg_n_style (N, S, ARG) but with S as
+ custom_quoting_style with left quote as LEFT_QUOTE and right quote
+ as RIGHT_QUOTE. See set_custom_quoting for a description of acceptable
+ LEFT_QUOTE and RIGHT_QUOTE values. */
+ #if 0
+-extern char *string_desc_quotearg_n_custom (int n,
+- char const *left_quote,
+- char const *right_quote,
+- string_desc_t arg);
++extern char *sd_quotearg_n_custom (int n,
++ char const *left_quote,
++ char const *right_quote,
++ string_desc_t arg);
+ #endif
+
+ /* Equivalent to
+- string_desc_quotearg_n_custom (0, LEFT_QUOTE, RIGHT_QUOTE, ARG). */
++ sd_quotearg_n_custom (0, LEFT_QUOTE, RIGHT_QUOTE, ARG). */
+ #if 0
+-extern char *string_desc_quotearg_custom (char const *left_quote,
+- char const *right_quote,
+- string_desc_t arg);
++extern char *sd_quotearg_custom (char const *left_quote,
++ char const *right_quote,
++ string_desc_t arg);
+ #endif
+
+
+ /* ==== Inline function definitions ==== */
+
+ GL_STRING_DESC_QUOTEARG_INLINE size_t
+-string_desc_quotearg_buffer (char *restrict buffer, size_t buffersize,
+- string_desc_t arg,
+- struct quoting_options const *o)
++sd_quotearg_buffer (char *restrict buffer, size_t buffersize,
++ string_desc_t arg,
++ struct quoting_options const *o)
+ {
+ return quotearg_buffer (buffer, buffersize,
+- string_desc_data (arg), string_desc_length (arg),
++ sd_data (arg), sd_length (arg),
+ o);
+ }
+
+@@ -147,69 +147,69 @@ _GL_ATTRIBUTE_NONNULL ((2))
+ _GL_ATTRIBUTE_MALLOC _GL_ATTRIBUTE_DEALLOC_FREE
+ _GL_ATTRIBUTE_RETURNS_NONNULL
+ char *
+-string_desc_quotearg_alloc (string_desc_t arg,
+- size_t *size,
+- struct quoting_options const *o)
++sd_quotearg_alloc (string_desc_t arg,
++ size_t *size,
++ struct quoting_options const *o)
+ {
+- return quotearg_alloc_mem (string_desc_data (arg), string_desc_length (arg),
++ return quotearg_alloc_mem (sd_data (arg), sd_length (arg),
+ size,
+ o);
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_n (int n, string_desc_t arg)
++sd_quotearg_n (int n, string_desc_t arg)
+ {
+- return quotearg_n_mem (n, string_desc_data (arg), string_desc_length (arg));
++ return quotearg_n_mem (n, sd_data (arg), sd_length (arg));
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg (string_desc_t arg)
++sd_quotearg (string_desc_t arg)
+ {
+- return quotearg_mem (string_desc_data (arg), string_desc_length (arg));
++ return quotearg_mem (sd_data (arg), sd_length (arg));
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_n_style (int n, enum quoting_style s, string_desc_t arg)
++sd_quotearg_n_style (int n, enum quoting_style s, string_desc_t arg)
+ {
+ return quotearg_n_style_mem (n, s,
+- string_desc_data (arg), string_desc_length (arg));
++ sd_data (arg), sd_length (arg));
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_style (enum quoting_style s, string_desc_t arg)
++sd_quotearg_style (enum quoting_style s, string_desc_t arg)
+ {
+ return quotearg_style_mem (s,
+- string_desc_data (arg), string_desc_length (arg));
++ sd_data (arg), sd_length (arg));
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_char (string_desc_t arg, char ch)
++sd_quotearg_char (string_desc_t arg, char ch)
+ {
+- return quotearg_char_mem (string_desc_data (arg), string_desc_length (arg),
++ return quotearg_char_mem (sd_data (arg), sd_length (arg),
+ ch);
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_colon (string_desc_t arg)
++sd_quotearg_colon (string_desc_t arg)
+ {
+- return quotearg_colon_mem (string_desc_data (arg), string_desc_length (arg));
++ return quotearg_colon_mem (sd_data (arg), sd_length (arg));
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_n_custom (int n,
+- char const *left_quote, char const *right_quote,
+- string_desc_t arg)
++sd_quotearg_n_custom (int n,
++ char const *left_quote, char const *right_quote,
++ string_desc_t arg)
+ {
+ return quotearg_n_custom_mem (n, left_quote, right_quote,
+- string_desc_data (arg), string_desc_length (arg));
++ sd_data (arg), sd_length (arg));
+ }
+
+ GL_STRING_DESC_QUOTEARG_INLINE char *
+-string_desc_quotearg_custom (char const *left_quote, char const *right_quote,
+- string_desc_t arg)
++sd_quotearg_custom (char const *left_quote, char const *right_quote,
++ string_desc_t arg)
+ {
+ return quotearg_custom_mem (left_quote, right_quote,
+- string_desc_data (arg), string_desc_length (arg));
++ sd_data (arg), sd_length (arg));
+ }
+
+
+--- a/lib/string-desc.c
++++ b/lib/string-desc.c
+@@ -39,14 +39,14 @@
+
+ /* Return true if A and B are equal. */
+ bool
+-string_desc_equals (string_desc_t a, string_desc_t b)
++sd_equals (string_desc_t a, string_desc_t b)
+ {
+ return (a._nbytes == b._nbytes
+ && (a._nbytes == 0 || memcmp (a._data, b._data, a._nbytes) == 0));
+ }
+
+ bool
+-string_desc_startswith (string_desc_t s, string_desc_t prefix)
++sd_startswith (string_desc_t s, string_desc_t prefix)
+ {
+ return (s._nbytes >= prefix._nbytes
+ && (prefix._nbytes == 0
+@@ -54,7 +54,7 @@ string_desc_startswith (string_desc_t s,
+ }
+
+ bool
+-string_desc_endswith (string_desc_t s, string_desc_t suffix)
++sd_endswith (string_desc_t s, string_desc_t suffix)
+ {
+ return (s._nbytes >= suffix._nbytes
+ && (suffix._nbytes == 0
+@@ -63,7 +63,7 @@ string_desc_endswith (string_desc_t s, s
+ }
+
+ int
+-string_desc_cmp (string_desc_t a, string_desc_t b)
++sd_cmp (string_desc_t a, string_desc_t b)
+ {
+ if (a._nbytes > b._nbytes)
+ {
+@@ -86,14 +86,14 @@ string_desc_cmp (string_desc_t a, string
+ }
+
+ int
+-string_desc_c_casecmp (string_desc_t a, string_desc_t b)
++sd_c_casecmp (string_desc_t a, string_desc_t b)
+ {
+ /* Don't use memcasecmp here, since it uses the current locale, not the
+ "C" locale. */
+- idx_t an = string_desc_length (a);
+- idx_t bn = string_desc_length (b);
+- const char *ap = string_desc_data (a);
+- const char *bp = string_desc_data (b);
++ idx_t an = sd_length (a);
++ idx_t bn = sd_length (b);
++ const char *ap = sd_data (a);
++ const char *bp = sd_data (b);
+ idx_t n = (an < bn ? an : bn);
+ idx_t i;
+ for (i = 0; i < n; i++)
+@@ -108,7 +108,7 @@ string_desc_c_casecmp (string_desc_t a,
+ }
+
+ ptrdiff_t
+-string_desc_index (string_desc_t s, char c)
++sd_index (string_desc_t s, char c)
+ {
+ if (s._nbytes > 0)
+ {
+@@ -120,7 +120,7 @@ string_desc_index (string_desc_t s, char
+ }
+
+ ptrdiff_t
+-string_desc_last_index (string_desc_t s, char c)
++sd_last_index (string_desc_t s, char c)
+ {
+ if (s._nbytes > 0)
+ {
+@@ -132,7 +132,7 @@ string_desc_last_index (string_desc_t s,
+ }
+
+ string_desc_t
+-string_desc_new_empty (void)
++sd_new_empty (void)
+ {
+ string_desc_t result;
+
+@@ -144,7 +144,7 @@ string_desc_new_empty (void)
+ }
+
+ string_desc_t
+-string_desc_new_addr (idx_t n, char *addr)
++sd_new_addr (idx_t n, char *addr)
+ {
+ string_desc_t result;
+
+@@ -158,7 +158,7 @@ string_desc_new_addr (idx_t n, char *add
+ }
+
+ string_desc_t
+-string_desc_from_c (const char *s)
++sd_from_c (const char *s)
+ {
+ string_desc_t result;
+
+@@ -169,7 +169,7 @@ string_desc_from_c (const char *s)
+ }
+
+ string_desc_t
+-string_desc_substring (string_desc_t s, idx_t start, idx_t end)
++sd_substring (string_desc_t s, idx_t start, idx_t end)
+ {
+ string_desc_t result;
+
+@@ -184,7 +184,7 @@ string_desc_substring (string_desc_t s,
+ }
+
+ int
+-string_desc_write (int fd, string_desc_t s)
++sd_write (int fd, string_desc_t s)
+ {
+ if (s._nbytes > 0)
+ if (full_write (fd, s._data, s._nbytes) != s._nbytes)
+@@ -194,7 +194,7 @@ string_desc_write (int fd, string_desc_t
+ }
+
+ int
+-string_desc_fwrite (FILE *fp, string_desc_t s)
++sd_fwrite (FILE *fp, string_desc_t s)
+ {
+ if (s._nbytes > 0)
+ if (fwrite (s._data, 1, s._nbytes, fp) != s._nbytes)
+@@ -206,7 +206,7 @@ string_desc_fwrite (FILE *fp, string_des
+ /* ==== Memory-allocating operations on string descriptors ==== */
+
+ int
+-string_desc_new (string_desc_t *resultp, idx_t n)
++sd_new (string_desc_t *resultp, idx_t n)
+ {
+ string_desc_t result;
+
+@@ -230,7 +230,7 @@ string_desc_new (string_desc_t *resultp,
+ }
+
+ int
+-string_desc_new_filled (string_desc_t *resultp, idx_t n, char c)
++sd_new_filled (string_desc_t *resultp, idx_t n, char c)
+ {
+ string_desc_t result;
+
+@@ -251,7 +251,7 @@ string_desc_new_filled (string_desc_t *r
+ }
+
+ int
+-string_desc_copy (string_desc_t *resultp, string_desc_t s)
++sd_copy (string_desc_t *resultp, string_desc_t s)
+ {
+ string_desc_t result;
+ idx_t n = s._nbytes;
+@@ -273,7 +273,7 @@ string_desc_copy (string_desc_t *resultp
+ }
+
+ int
+-string_desc_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...)
++sd_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...)
+ {
+ if (n <= 0)
+ /* Invalid argument. */
+@@ -327,7 +327,7 @@ string_desc_concat (string_desc_t *resul
+ }
+
+ char *
+-string_desc_c (string_desc_t s)
++sd_c (string_desc_t s)
+ {
+ idx_t n = s._nbytes;
+ char *result = (char *) imalloc (n + 1);
+@@ -345,7 +345,7 @@ string_desc_c (string_desc_t s)
+ /* ==== Operations with side effects on string descriptors ==== */
+
+ void
+-string_desc_set_char_at (string_desc_t s, idx_t i, char c)
++sd_set_char_at (string_desc_t s, idx_t i, char c)
+ {
+ if (!(i >= 0 && i < s._nbytes))
+ /* Invalid argument. */
+@@ -354,7 +354,7 @@ string_desc_set_char_at (string_desc_t s
+ }
+
+ void
+-string_desc_fill (string_desc_t s, idx_t start, idx_t end, char c)
++sd_fill (string_desc_t s, idx_t start, idx_t end, char c)
+ {
+ if (!(start >= 0 && start <= end))
+ /* Invalid arguments. */
+@@ -365,7 +365,7 @@ string_desc_fill (string_desc_t s, idx_t
+ }
+
+ void
+-string_desc_overwrite (string_desc_t s, idx_t start, string_desc_t t)
++sd_overwrite (string_desc_t s, idx_t start, string_desc_t t)
+ {
+ if (!(start >= 0 && start + t._nbytes <= s._nbytes))
+ /* Invalid arguments. */
+@@ -376,7 +376,7 @@ string_desc_overwrite (string_desc_t s,
+ }
+
+ void
+-string_desc_free (string_desc_t s)
++sd_free (string_desc_t s)
+ {
+ free (s._data);
+ }
+--- a/lib/string-desc.h
++++ b/lib/string-desc.h
+@@ -69,82 +69,82 @@ struct string_desc_t
+
+ /* Return the length of the string S. */
+ #if 0 /* Defined inline below. */
+-extern idx_t string_desc_length (string_desc_t s);
++extern idx_t sd_length (string_desc_t s);
+ #endif
+
+ /* Return the byte at index I of string S.
+ I must be < length(S). */
+ #if 0 /* Defined inline below. */
+-extern char string_desc_char_at (string_desc_t s, idx_t i);
++extern char sd_char_at (string_desc_t s, idx_t i);
+ #endif
+
+ /* Return a read-only view of the bytes of S. */
+ #if 0 /* Defined inline below. */
+-extern const char * string_desc_data (string_desc_t s);
++extern const char * sd_data (string_desc_t s);
+ #endif
+
+ /* Return true if S is the empty string. */
+ #if 0 /* Defined inline below. */
+-extern bool string_desc_is_empty (string_desc_t s);
++extern bool sd_is_empty (string_desc_t s);
+ #endif
+
+ /* Return true if A and B are equal. */
+-extern bool string_desc_equals (string_desc_t a, string_desc_t b);
++extern bool sd_equals (string_desc_t a, string_desc_t b);
+
+ /* Return true if S starts with PREFIX. */
+-extern bool string_desc_startswith (string_desc_t s, string_desc_t prefix);
++extern bool sd_startswith (string_desc_t s, string_desc_t prefix);
+
+ /* Return true if S ends with SUFFIX. */
+-extern bool string_desc_endswith (string_desc_t s, string_desc_t suffix);
++extern bool sd_endswith (string_desc_t s, string_desc_t suffix);
+
+ /* Return > 0, == 0, or < 0 if A > B, A == B, A < B.
+ This uses a lexicographic ordering, where the bytes are compared as
+ 'unsigned char'. */
+-extern int string_desc_cmp (string_desc_t a, string_desc_t b);
++extern int sd_cmp (string_desc_t a, string_desc_t b);
+
+ /* Return > 0, == 0, or < 0 if A > B, A == B, A < B.
+ Either A or B must be entirely ASCII.
+ This uses a lexicographic ordering, where the bytes are compared as
+ 'unsigned char', ignoring case, in the "C" locale. */
+-extern int string_desc_c_casecmp (string_desc_t a, string_desc_t b);
++extern int sd_c_casecmp (string_desc_t a, string_desc_t b);
+
+ /* Return the index of the first occurrence of C in S,
+ or -1 if there is none. */
+-extern ptrdiff_t string_desc_index (string_desc_t s, char c);
++extern ptrdiff_t sd_index (string_desc_t s, char c);
+
+ /* Return the index of the last occurrence of C in S,
+ or -1 if there is none. */
+-extern ptrdiff_t string_desc_last_index (string_desc_t s, char c);
++extern ptrdiff_t sd_last_index (string_desc_t s, char c);
+
+ /* Return the index of the first occurrence of NEEDLE in HAYSTACK,
+ or -1 if there is none. */
+-extern ptrdiff_t string_desc_contains (string_desc_t haystack, string_desc_t needle);
++extern ptrdiff_t sd_contains (string_desc_t haystack, string_desc_t needle);
+
+ /* Return an empty string. */
+-extern string_desc_t string_desc_new_empty (void);
++extern string_desc_t sd_new_empty (void);
+
+ /* Construct and return a string of length N, at the given memory address. */
+-extern string_desc_t string_desc_new_addr (idx_t n, char *addr);
++extern string_desc_t sd_new_addr (idx_t n, char *addr);
+
+ /* Return a string that represents the C string S, of length strlen (S). */
+-extern string_desc_t string_desc_from_c (const char *s);
++extern string_desc_t sd_from_c (const char *s);
+
+ /* Return the substring of S, starting at offset START and ending at offset END.
+ START must be <= END.
+ The result is of length END - START.
+ The result must not be freed (since its storage is part of the storage
+ of S). */
+-extern string_desc_t string_desc_substring (string_desc_t s, idx_t start, idx_t end);
++extern string_desc_t sd_substring (string_desc_t s, idx_t start, idx_t end);
+
+ /* Output S to the file descriptor FD.
+ Return 0 if successful.
+ Upon error, return -1 with errno set. */
+-extern int string_desc_write (int fd, string_desc_t s);
++extern int sd_write (int fd, string_desc_t s);
+
+ /* Output S to the FILE stream FP.
+ Return 0 if successful.
+ Upon error, return -1. */
+-extern int string_desc_fwrite (FILE *fp, string_desc_t s);
++extern int sd_fwrite (FILE *fp, string_desc_t s);
+
+
+ /* ==== Memory-allocating operations on string descriptors ==== */
+@@ -153,61 +153,61 @@ extern int string_desc_fwrite (FILE *fp,
+ Return 0 if successful.
+ Upon error, return -1 with errno set. */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_new (string_desc_t *resultp, idx_t n);
++extern int sd_new (string_desc_t *resultp, idx_t n);
+
+ /* Construct a string of length N, filled with C.
+ Return 0 if successful.
+ Upon error, return -1 with errno set. */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_new_filled (string_desc_t *resultp, idx_t n, char c);
++extern int sd_new_filled (string_desc_t *resultp, idx_t n, char c);
+
+ /* Construct a copy of string S.
+ Return 0 if successful.
+ Upon error, return -1 with errno set. */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_copy (string_desc_t *resultp, string_desc_t s);
++extern int sd_copy (string_desc_t *resultp, string_desc_t s);
+
+ /* Construct the concatenation of N strings. N must be > 0.
+ Return 0 if successful.
+ Upon error, return -1 with errno set. */
+ _GL_ATTRIBUTE_NODISCARD
+-extern int string_desc_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...);
++extern int sd_concat (string_desc_t *resultp, idx_t n, string_desc_t string1, ...);
+
+ /* Construct a copy of string S, as a NUL-terminated C string.
+ Return it is successful.
+ Upon error, return NULL with errno set. */
+-extern char * string_desc_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
++extern char * sd_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
+
+
+ /* ==== Operations with side effects on string descriptors ==== */
+
+ /* Overwrite the byte at index I of string S with C.
+ I must be < length(S). */
+-extern void string_desc_set_char_at (string_desc_t s, idx_t i, char c);
++extern void sd_set_char_at (string_desc_t s, idx_t i, char c);
+
+ /* Fill part of S, starting at offset START and ending at offset END,
+ with copies of C.
+ START must be <= END. */
+-extern void string_desc_fill (string_desc_t s, idx_t start, idx_t end, char c);
++extern void sd_fill (string_desc_t s, idx_t start, idx_t end, char c);
+
+ /* Overwrite part of S with T, starting at offset START.
+ START + length(T) must be <= length (S). */
+-extern void string_desc_overwrite (string_desc_t s, idx_t start, string_desc_t t);
++extern void sd_overwrite (string_desc_t s, idx_t start, string_desc_t t);
+
+ /* Free S. */
+-extern void string_desc_free (string_desc_t s);
++extern void sd_free (string_desc_t s);
+
+
+ /* ==== Inline function definitions ==== */
+
+ GL_STRING_DESC_INLINE idx_t
+-string_desc_length (string_desc_t s)
++sd_length (string_desc_t s)
+ {
+ return s._nbytes;
+ }
+
+ GL_STRING_DESC_INLINE char
+-string_desc_char_at (string_desc_t s, idx_t i)
++sd_char_at (string_desc_t s, idx_t i)
+ {
+ if (!(i >= 0 && i < s._nbytes))
+ /* Invalid argument. */
+@@ -216,13 +216,13 @@ string_desc_char_at (string_desc_t s, id
+ }
+
+ GL_STRING_DESC_INLINE const char *
+-string_desc_data (string_desc_t s)
++sd_data (string_desc_t s)
+ {
+ return s._data;
+ }
+
+ GL_STRING_DESC_INLINE bool
+-string_desc_is_empty (string_desc_t s)
++sd_is_empty (string_desc_t s)
+ {
+ return s._nbytes == 0;
+ }
+--- a/lib/xstring-buffer.c
++++ b/lib/xstring-buffer.c
+@@ -59,10 +59,10 @@ sb_xdupfree (struct string_buffer *buffe
+ if (buffer->error)
+ {
+ sb_free (buffer);
+- return string_desc_new_addr (0, NULL);
++ return sd_new_addr (0, NULL);
+ }
+ string_desc_t contents = sb_dupfree (buffer);
+- if (string_desc_data (contents) == NULL)
++ if (sd_data (contents) == NULL)
+ xalloc_die ();
+ return contents;
+ }
+--- a/lib/xstring-desc.c
++++ b/lib/xstring-desc.c
+@@ -22,7 +22,7 @@
+ #include "ialloc.h"
+
+ string_desc_t
+-xstring_desc_concat (idx_t n, string_desc_t string1, ...)
++xsd_concat (idx_t n, string_desc_t string1, ...)
+ {
+ if (n <= 0)
+ /* Invalid argument. */
+--- a/lib/xstring-desc.h
++++ b/lib/xstring-desc.h
+@@ -43,53 +43,53 @@ extern "C" {
+
+ /* Return a string of length N, with uninitialized contents. */
+ #if 0 /* Defined inline below. */
+-extern string_desc_t xstring_desc_new (idx_t n);
++extern string_desc_t xsd_new (idx_t n);
+ #endif
+
+ /* Return a string of length N, filled with C. */
+ #if 0 /* Defined inline below. */
+-extern string_desc_t xstring_desc_new_filled (idx_t n, char c);
++extern string_desc_t xsd_new_filled (idx_t n, char c);
+ #endif
+
+ /* Return a copy of string S. */
+ #if 0 /* Defined inline below. */
+-extern string_desc_t xstring_desc_copy (string_desc_t s);
++extern string_desc_t xsd_copy (string_desc_t s);
+ #endif
+
+ /* Return the concatenation of N strings. N must be > 0. */
+-extern string_desc_t xstring_desc_concat (idx_t n, string_desc_t string1, ...);
++extern string_desc_t xsd_concat (idx_t n, string_desc_t string1, ...);
+
+ /* Construct and return a copy of string S, as a NUL-terminated C string. */
+ #if 0 /* Defined inline below. */
+-extern char * xstring_desc_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
++extern char * xsd_c (string_desc_t s) _GL_ATTRIBUTE_DEALLOC_FREE;
+ #endif
+
+
+ /* ==== Inline function definitions ==== */
+
+ GL_XSTRING_DESC_INLINE string_desc_t
+-xstring_desc_new (idx_t n)
++xsd_new (idx_t n)
+ {
+ string_desc_t result;
+- if (string_desc_new (&result, n) < 0)
++ if (sd_new (&result, n) < 0)
+ xalloc_die ();
+ return result;
+ }
+
+ GL_XSTRING_DESC_INLINE string_desc_t
+-xstring_desc_new_filled (idx_t n, char c)
++xsd_new_filled (idx_t n, char c)
+ {
+ string_desc_t result;
+- if (string_desc_new_filled (&result, n, c) < 0)
++ if (sd_new_filled (&result, n, c) < 0)
+ xalloc_die ();
+ return result;
+ }
+
+ GL_XSTRING_DESC_INLINE string_desc_t
+-xstring_desc_copy (string_desc_t s)
++xsd_copy (string_desc_t s)
+ {
+ string_desc_t result;
+- if (string_desc_copy (&result, s) < 0)
++ if (sd_copy (&result, s) < 0)
+ xalloc_die ();
+ return result;
+ }
+@@ -97,9 +97,9 @@ xstring_desc_copy (string_desc_t s)
+ GL_XSTRING_DESC_INLINE
+ _GL_ATTRIBUTE_DEALLOC_FREE
+ char *
+-xstring_desc_c (string_desc_t s)
++xsd_c (string_desc_t s)
+ {
+- char *result = string_desc_c (s);
++ char *result = sd_c (s);
+ if (result == NULL)
+ xalloc_die ();
+ return result;
+--- a/tests/test-string-desc-quotearg.c
++++ b/tests/test-string-desc-quotearg.c
+@@ -28,75 +28,75 @@
+ int
+ main (void)
+ {
+- string_desc_t s1 = string_desc_from_c ("Hello world!");
+- string_desc_t s2 = string_desc_new_addr (21, "The\0quick\0brown\0\0fox");
++ string_desc_t s1 = sd_from_c ("Hello world!");
++ string_desc_t s2 = sd_new_addr (21, "The\0quick\0brown\0\0fox");
+
+- /* Test string_desc_quotearg_buffer. */
++ /* Test sd_quotearg_buffer. */
+ {
+ char buf[80];
+- size_t n = string_desc_quotearg_buffer (buf, sizeof (buf), s2, NULL);
++ size_t n = sd_quotearg_buffer (buf, sizeof (buf), s2, NULL);
+ ASSERT (n == 21);
+ ASSERT (memcmp (buf, "The\0quick\0brown\0\0fox", n) == 0);
+ }
+
+- /* Test string_desc_quotearg_alloc. */
++ /* Test sd_quotearg_alloc. */
+ {
+ size_t n;
+- char *ret = string_desc_quotearg_alloc (s2, &n, NULL);
++ char *ret = sd_quotearg_alloc (s2, &n, NULL);
+ ASSERT (n == 21);
+ ASSERT (memcmp (ret, "The\0quick\0brown\0\0fox", n) == 0);
+ free (ret);
+ }
+
+- /* Test string_desc_quotearg_n. */
++ /* Test sd_quotearg_n. */
+ {
+- char *ret = string_desc_quotearg_n (1, s2);
++ char *ret = sd_quotearg_n (1, s2);
+ ASSERT (memcmp (ret, "Thequickbrownfox", 16 + 1) == 0);
+ }
+
+- /* Test string_desc_quotearg. */
++ /* Test sd_quotearg. */
+ {
+- char *ret = string_desc_quotearg (s2);
++ char *ret = sd_quotearg (s2);
+ ASSERT (memcmp (ret, "Thequickbrownfox", 16 + 1) == 0);
+ }
+
+- /* Test string_desc_quotearg_n_style. */
++ /* Test sd_quotearg_n_style. */
+ {
+- char *ret = string_desc_quotearg_n_style (1, clocale_quoting_style, s2);
++ char *ret = sd_quotearg_n_style (1, clocale_quoting_style, s2);
+ ASSERT (memcmp (ret, "\"The\\0quick\\0brown\\0\\0fox\\0\"", 28 + 1) == 0
+ || /* if the locale has UTF-8 encoding */
+ memcmp (ret, "\342\200\230The\\0quick\\0brown\\0\\0fox\\0\342\200\231", 32 + 1) == 0);
+ }
+
+- /* Test string_desc_quotearg_style. */
++ /* Test sd_quotearg_style. */
+ {
+- char *ret = string_desc_quotearg_style (clocale_quoting_style, s2);
++ char *ret = sd_quotearg_style (clocale_quoting_style, s2);
+ ASSERT (memcmp (ret, "\"The\\0quick\\0brown\\0\\0fox\\0\"", 28 + 1) == 0
+ || /* if the locale has UTF-8 encoding */
+ memcmp (ret, "\342\200\230The\\0quick\\0brown\\0\\0fox\\0\342\200\231", 32 + 1) == 0);
+ }
+
+- /* Test string_desc_quotearg_char. */
++ /* Test sd_quotearg_char. */
+ {
+- char *ret = string_desc_quotearg_char (s1, ' ');
++ char *ret = sd_quotearg_char (s1, ' ');
+ ASSERT (memcmp (ret, "Hello world!", 12 + 1) == 0); /* ' ' not quoted?! */
+ }
+
+- /* Test string_desc_quotearg_colon. */
++ /* Test sd_quotearg_colon. */
+ {
+- char *ret = string_desc_quotearg_colon (string_desc_from_c ("a:b"));
++ char *ret = sd_quotearg_colon (sd_from_c ("a:b"));
+ ASSERT (memcmp (ret, "a:b", 3 + 1) == 0); /* ':' not quoted?! */
+ }
+
+- /* Test string_desc_quotearg_n_custom. */
++ /* Test sd_quotearg_n_custom. */
+ {
+- char *ret = string_desc_quotearg_n_custom (2, "<", ">", s1);
++ char *ret = sd_quotearg_n_custom (2, "<", ">", s1);
+ ASSERT (memcmp (ret, "<Hello world!>", 14 + 1) == 0);
+ }
+
+- /* Test string_desc_quotearg_n_custom. */
++ /* Test sd_quotearg_n_custom. */
+ {
+- char *ret = string_desc_quotearg_custom ("[[", "]]", s1);
++ char *ret = sd_quotearg_custom ("[[", "]]", s1);
+ ASSERT (memcmp (ret, "[[Hello world!]]", 16 + 1) == 0);
+ }
+
+--- a/tests/test-string-desc.sh
++++ b/tests/test-string-desc.sh
+@@ -6,7 +6,7 @@ ${CHECKER} test-string-desc${EXEEXT} tes
+ printf 'Hello world!The\0quick\0brown\0\0fox\0' > test-string-desc.ok
+
+ : "${DIFF=diff}"
+-${DIFF} test-string-desc.ok test-string-desc-1.tmp || { echo "string_desc_fwrite KO" 1>&2; Exit 1; }
+-${DIFF} test-string-desc.ok test-string-desc-3.tmp || { echo "string_desc_write KO" 1>&2; Exit 1; }
++${DIFF} test-string-desc.ok test-string-desc-1.tmp || { echo "sd_fwrite KO" 1>&2; Exit 1; }
++${DIFF} test-string-desc.ok test-string-desc-3.tmp || { echo "sd_write KO" 1>&2; Exit 1; }
+
+ Exit 0
+--- a/tests/test-xstring-desc.c
++++ b/tests/test-xstring-desc.c
+@@ -28,53 +28,53 @@
+ int
+ main (void)
+ {
+- string_desc_t s0 = string_desc_new_empty ();
+- string_desc_t s1 = string_desc_from_c ("Hello world!");
+- string_desc_t s2 = string_desc_new_addr (21, "The\0quick\0brown\0\0fox");
++ string_desc_t s0 = sd_new_empty ();
++ string_desc_t s1 = sd_from_c ("Hello world!");
++ string_desc_t s2 = sd_new_addr (21, "The\0quick\0brown\0\0fox");
+
+- /* Test xstring_desc_new. */
+- string_desc_t s4 = xstring_desc_new (5);
+- string_desc_set_char_at (s4, 0, 'H');
+- string_desc_set_char_at (s4, 4, 'o');
+- string_desc_set_char_at (s4, 1, 'e');
+- string_desc_fill (s4, 2, 4, 'l');
+- ASSERT (string_desc_length (s4) == 5);
+- ASSERT (string_desc_startswith (s1, s4));
++ /* Test xsd_new. */
++ string_desc_t s4 = xsd_new (5);
++ sd_set_char_at (s4, 0, 'H');
++ sd_set_char_at (s4, 4, 'o');
++ sd_set_char_at (s4, 1, 'e');
++ sd_fill (s4, 2, 4, 'l');
++ ASSERT (sd_length (s4) == 5);
++ ASSERT (sd_startswith (s1, s4));
+
+- /* Test xstring_desc_new_filled. */
+- string_desc_t s5 = xstring_desc_new_filled (5, 'l');
+- string_desc_set_char_at (s5, 0, 'H');
+- string_desc_set_char_at (s5, 4, 'o');
+- string_desc_set_char_at (s5, 1, 'e');
+- ASSERT (string_desc_length (s5) == 5);
+- ASSERT (string_desc_startswith (s1, s5));
++ /* Test xsd_new_filled. */
++ string_desc_t s5 = xsd_new_filled (5, 'l');
++ sd_set_char_at (s5, 0, 'H');
++ sd_set_char_at (s5, 4, 'o');
++ sd_set_char_at (s5, 1, 'e');
++ ASSERT (sd_length (s5) == 5);
++ ASSERT (sd_startswith (s1, s5));
+
+- /* Test xstring_desc_copy. */
++ /* Test xsd_copy. */
+ {
+- string_desc_t s6 = xstring_desc_copy (s0);
+- ASSERT (string_desc_is_empty (s6));
+- string_desc_free (s6);
++ string_desc_t s6 = xsd_copy (s0);
++ ASSERT (sd_is_empty (s6));
++ sd_free (s6);
+ }
+ {
+- string_desc_t s6 = xstring_desc_copy (s2);
+- ASSERT (string_desc_equals (s6, s2));
+- string_desc_free (s6);
++ string_desc_t s6 = xsd_copy (s2);
++ ASSERT (sd_equals (s6, s2));
++ sd_free (s6);
+ }
+
+- /* Test xstring_desc_concat. */
++ /* Test xsd_concat. */
+ {
+ string_desc_t s8 =
+- xstring_desc_concat (3, string_desc_new_addr (10, "The\0quick"),
+- string_desc_new_addr (7, "brown\0"),
+- string_desc_new_addr (4, "fox"),
+- string_desc_new_addr (7, "unused"));
+- ASSERT (string_desc_equals (s8, s2));
+- string_desc_free (s8);
++ xsd_concat (3, sd_new_addr (10, "The\0quick"),
++ sd_new_addr (7, "brown\0"),
++ sd_new_addr (4, "fox"),
++ sd_new_addr (7, "unused"));
++ ASSERT (sd_equals (s8, s2));
++ sd_free (s8);
+ }
+
+- /* Test xstring_desc_c. */
++ /* Test xsd_c. */
+ {
+- char *ptr = xstring_desc_c (s2);
++ char *ptr = xsd_c (s2);
+ ASSERT (ptr != NULL);
+ ASSERT (memcmp (ptr, "The\0quick\0brown\0\0fox\0", 22) == 0);
+ free (ptr);
--- /dev/null
+From 60cd34886c2c9f509974239fcf64a61f9a507d14 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Tue, 25 Feb 2025 09:04:28 +0100
+Subject: [PATCH] vc-mtime: Reduce number of read() system calls.
+
+* lib/vc-mtime.c: Include <stddef.h>.
+(git_vc_controlled): Read bytes into a buffer, not one-by-one.
+---
+ ChangeLog | 6 ++++++
+ lib/vc-mtime.c | 15 +++++++++++----
+ 2 files changed, 17 insertions(+), 4 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -21,6 +21,7 @@
+ /* Specification. */
+ #include "vc-mtime.h"
+
++#include <stddef.h>
+ #include <stdlib.h>
+ #include <unistd.h>
+
+@@ -56,11 +57,17 @@ git_vc_controlled (const char *filename)
+ return false;
+
+ /* Read the subprocess output, and test whether it is non-empty. */
+- size_t count = 0;
+- char c;
++ ptrdiff_t count = 0;
+
+- while (safe_read (fd[0], &c, 1) > 0)
+- count++;
++ for (;;)
++ {
++ char buf[1024];
++ ptrdiff_t n = safe_read (fd[0], buf, sizeof (buf));
++ if (n > 0)
++ count += n;
++ else
++ break;
++ }
+
+ close (fd[0]);
+
--- /dev/null
+From 78269749030dde23182c29376d1410592436eb5d Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Thu, 1 May 2025 17:26:27 +0200
+Subject: [PATCH] vc-mtime: Add API for more efficient use of git.
+
+Reported by Serhii Tereshchenko, Arthur, Adam YS, Foucauld Degeorges
+at <https://savannah.gnu.org/bugs/?66865>.
+
+* lib/vc-mtime.h (max_vc_mtime): New declaration.
+* lib/vc-mtime.c: Include <errno.h>, <stdio.h>, <string.h>, filename.h,
+xalloc.h, xgetcwd.h, xvasprintf.h, gl_map.h, gl_xmap.h, gl_hash_map.h,
+hashkey-string.h, unlocked-io.h.
+(is_git_present): New function, extracted from vc_mtime.
+(vc_mtime): Invoke it.
+(MAX_COMMAND_LENGTH, MAX_CMD_LEN): New macros.
+(abs_git_checkout): New function, based on execute_and_read_line in
+lib/javacomp.c.
+(ancestor_level, relativize): New functions.
+(struct accumulator): New type.
+(accumulate): New function.
+(max_vc_mtime): New function.
+(test_ancestor_level, test_relativize, main) [TEST]: New functions.
+* modules/vc-mtime (Depends-on): Add filename, xalloc, xgetcwd,
+canonicalize-lgpl, xvasprintf, str_startswith, map, xmap, hash-map,
+hashkey-string, getdelim.
+---
+ ChangeLog | 23 ++
+ lib/vc-mtime.c | 866 +++++++++++++++++++++++++++++++++++++++++++++--
+ lib/vc-mtime.h | 7 +
+ modules/vc-mtime | 11 +
+ 4 files changed, 886 insertions(+), 21 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -21,8 +21,11 @@
+ /* Specification. */
+ #include "vc-mtime.h"
+
++#include <errno.h>
+ #include <stddef.h>
++#include <stdio.h>
+ #include <stdlib.h>
++#include <string.h>
+ #include <unistd.h>
+
+ #include <error.h>
+@@ -32,11 +35,51 @@
+ #include "safe-read.h"
+ #include "xstrtol.h"
+ #include "stat-time.h"
++#include "filename.h"
++#include "xalloc.h"
++#include "xgetcwd.h"
++#include "xvasprintf.h"
++#include "gl_map.h"
++#include "gl_xmap.h"
++#include "gl_hash_map.h"
++#include "hashkey-string.h"
++#if USE_UNLOCKED_IO
++# include "unlocked-io.h"
++#endif
+ #include "gettext.h"
+
+ #define _(msgid) dgettext ("gnulib", msgid)
+
+
++/* ========================================================================== */
++
++/* Determines whether git is present. */
++static bool
++is_git_present (void)
++{
++ static bool git_tested;
++ static bool git_present;
++
++ if (!git_tested)
++ {
++ /* Test for presence of git:
++ "git --version >/dev/null 2>/dev/null" */
++ const char *argv[3];
++ int exitstatus;
++
++ argv[0] = "git";
++ argv[1] = "--version";
++ argv[2] = NULL;
++ exitstatus = execute ("git", "git", argv, NULL, NULL,
++ false, false, true, true,
++ true, false, NULL);
++ git_present = (exitstatus == 0);
++ git_tested = true;
++ }
++
++ return git_present;
++}
++
+ /* Determines whether the specified file is under version control. */
+ static bool
+ git_vc_controlled (const char *filename)
+@@ -178,27 +221,7 @@ git_mtime (struct timespec *mtime, const
+ int
+ vc_mtime (struct timespec *mtime, const char *filename)
+ {
+- static bool git_tested;
+- static bool git_present;
+-
+- if (!git_tested)
+- {
+- /* Test for presence of git:
+- "git --version >/dev/null 2>/dev/null" */
+- const char *argv[3];
+- int exitstatus;
+-
+- argv[0] = "git";
+- argv[1] = "--version";
+- argv[2] = NULL;
+- exitstatus = execute ("git", "git", argv, NULL, NULL,
+- false, false, true, true,
+- true, false, NULL);
+- git_present = (exitstatus == 0);
+- git_tested = true;
+- }
+-
+- if (git_present
++ if (is_git_present ()
+ && git_vc_controlled (filename)
+ && git_unmodified (filename))
+ {
+@@ -213,3 +236,804 @@ vc_mtime (struct timespec *mtime, const
+ }
+ return -1;
+ }
++
++/* ========================================================================== */
++
++/* Maximum length of a command that is guaranteed to work. */
++#if defined _WIN32 || defined __CYGWIN__
++/* Windows */
++# define MAX_COMMAND_LENGTH 8192
++#else
++/* Unix platforms */
++# define MAX_COMMAND_LENGTH 32768
++#endif
++/* Keep some safe distance to this maximum. */
++#define MAX_CMD_LEN ((int) (MAX_COMMAND_LENGTH * 0.8))
++
++/* Returns the directory name of the git checkout that contains tha current
++ directory, as an absolute file name, or NULL if the current directory is
++ not in a git checkout. */
++static char *
++abs_git_checkout (void)
++{
++ /* Run "git rev-parse --show-toplevel 2>/dev/null" and return its output,
++ without the trailing newline. */
++ const char *argv[4];
++ pid_t child;
++ int fd[1];
++
++ argv[0] = "git";
++ argv[1] = "rev-parse";
++ argv[2] = "--show-toplevel";
++ argv[3] = NULL;
++ child = create_pipe_in ("git", "git", argv, NULL, NULL,
++ DEV_NULL, true, true, false, fd);
++
++ if (child == -1)
++ return NULL;
++
++ /* Retrieve its result. */
++ FILE *fp = fdopen (fd[0], "r");
++ if (fp == NULL)
++ error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++ char *line = NULL;
++ size_t linesize = 0;
++ size_t linelen = getline (&line, &linesize, fp);
++ if (linelen == (size_t)(-1))
++ {
++ fclose (fp);
++ wait_subprocess (child, "git", true, true, true, false, NULL);
++ return NULL;
++ }
++ else
++ {
++ int exitstatus;
++
++ if (linelen > 0 && line[linelen - 1] == '\n')
++ line[linelen - 1] = '\0';
++
++ /* Read until EOF (otherwise the child process may get a SIGPIPE signal). */
++ while (getc (fp) != EOF)
++ ;
++
++ fclose (fp);
++
++ /* Remove zombie process from process list, and retrieve exit status. */
++ exitstatus =
++ wait_subprocess (child, "git", true, true, true, false, NULL);
++ if (exitstatus == 0)
++ return line;
++ }
++ free (line);
++ return NULL;
++}
++
++/* Given an absolute canonicalized directory DIR1 and an absolute canonicalized
++ directory DIR2, returns N where DIR1 = DIR2 "/.." ... "/.." with N times
++ "/..", or -1 if DIR1 is not an ancestor directory of DIR2. */
++static long
++ancestor_level (const char *dir1, const char *dir2)
++{
++ if (strcmp (dir1, "/") == 0)
++ dir1 = "";
++ if (strcmp (dir2, "/") == 0)
++ dir2 = "";
++ size_t dir1_len = strlen (dir1);
++ if (strncmp (dir1, dir2, dir1_len) == 0)
++ {
++ /* DIR2 starts with DIR1. */
++ const char *p = dir2 + dir1_len;
++ if (*p == '\0')
++ /* DIR2 and DIR1 are the same. */
++ return 0;
++ if (ISSLASH (*p))
++ {
++ /* Return the number of slashes in the tail of DIR2 that starts
++ at P. */
++ long n = 1;
++ p++;
++ for (; *p != '\0'; p++)
++ if (ISSLASH (*p))
++ n++;
++ return n;
++ }
++ }
++ return -1;
++}
++
++/* Given an absolute canolicalized FILENAME that starts with DIR1, returns the
++ same file name relative to DIR2, where DIR1 = DIR2 "/.." ... "/.." with
++ N times "/..", as a freshly allocated string. */
++static char *
++relativize (const char *filename,
++ unsigned long n, const char *dir1, const char *dir2)
++{
++ if (strcmp (dir1, "/") == 0)
++ dir1 = "";
++ size_t dir1_len = strlen (dir1);
++ if (!(strncmp (filename, dir1, dir1_len) == 0
++ && (filename[dir1_len] == '\0' || ISSLASH (filename[dir1_len]))))
++ /* Invalid argument. */
++ abort ();
++ if (strcmp (dir2, "/") == 0)
++ dir2 = "";
++
++ dir2 += dir1_len;
++ filename += dir1_len;
++ for (;;)
++ {
++ /* Invariant: The result will be N times "../" followed by FILENAME. */
++ if (*filename == '\0')
++ break;
++ if (!ISSLASH (*filename))
++ abort ();
++ filename++;
++ if (*dir2 == '\0')
++ break;
++ if (!ISSLASH (*dir2))
++ abort ();
++ dir2++;
++ /* Skip one component in DIR2. */
++ const char *dir2_s;
++ for (dir2_s = dir2; *dir2_s != '\0'; dir2_s++)
++ if (ISSLASH (*dir2_s))
++ break;
++ /* Skip one component in FILENAME, at P. */
++ const char *filename_s;
++ for (filename_s = filename; *filename_s != '\0'; filename_s++)
++ if (ISSLASH (*filename_s))
++ break;
++ /* Did the components match? */
++ if (!(filename_s - filename == dir2_s - dir2
++ && memcmp (filename, dir2, dir2_s - dir2) == 0))
++ break;
++ dir2 = dir2_s;
++ filename = filename_s;
++ n--;
++ }
++
++ if (n == 0 && *filename == '\0')
++ return xstrdup (".");
++
++ char *result = (char *) xmalloc (3 * n + strlen (filename) + 1);
++ {
++ char *q = result;
++ for (; n > 0; n--)
++ {
++ q[0] = '.'; q[1] = '.'; q[2] = '/'; q += 3;
++ }
++ strcpy (q, filename);
++ }
++ return result;
++}
++
++/* Accumulating mtimes. */
++struct accumulator
++{
++ bool has_some_mtimes;
++ struct timespec max_of_mtimes;
++};
++
++static void
++accumulate (struct accumulator *accu, struct timespec mtime)
++{
++ if (accu->has_some_mtimes)
++ {
++ /* Compute the maximum of accu->max_of_mtimes and mtime. */
++ if (accu->max_of_mtimes.tv_sec < mtime.tv_sec
++ || (accu->max_of_mtimes.tv_sec == mtime.tv_sec
++ && accu->max_of_mtimes.tv_nsec < mtime.tv_nsec))
++ accu->max_of_mtimes = mtime;
++ }
++ else
++ {
++ accu->max_of_mtimes = mtime;
++ accu->has_some_mtimes = true;
++ }
++}
++
++int
++max_vc_mtime (struct timespec *max_of_mtimes,
++ size_t nfiles, const char * const *filenames)
++{
++ if (nfiles == 0)
++ /* Invalid argument. */
++ abort ();
++
++ struct accumulator accu = { false };
++
++ /* Determine which of the specified files are under version control,
++ and which are duplicates. (The case of duplicates is rare, but it needs
++ special attention, because 'git ls-files' eliminates duplicates.)
++ vc_controlled[n] = 1 means that filenames[n] is under version control.
++ vc_controlled[n] = 0 means that filenames[n] is not under version control.
++ vc_controlled[n] = -1 means that filenames[n] is a duplicate. */
++ signed char *vc_controlled = XNMALLOC (nfiles, signed char);
++ for (size_t n = 0; n < nfiles; n++)
++ vc_controlled[n] = 0;
++
++ if (is_git_present ())
++ {
++ /* Since 'git ls-files' produces an error when at least one of the files
++ is outside the git checkout that contains tha current directory, we
++ need to filter out such files. This is most easily done by converting
++ each file name to a canonical file name first and then comparing with
++ the directory name of said git checkout. */
++ char *git_checkout = abs_git_checkout ();
++ if (git_checkout != NULL)
++ {
++ char *currdir = xgetcwd ();
++ /* git_checkout is expected to be an ancestor directory of the
++ current directory. */
++ long ancestor = ancestor_level (git_checkout, currdir);
++ if (ancestor >= 0)
++ {
++ char **canonical_filenames = XNMALLOC (nfiles, char *);
++ for (size_t n = 0; n < nfiles; n++)
++ {
++ char *canonical = canonicalize_file_name (filenames[n]);
++ if (canonical == NULL)
++ {
++ if (errno == ENOMEM)
++ xalloc_die ();
++ /* The file filenames[n] does not exist. */
++ for (size_t k = n; k > 0; )
++ free (canonical_filenames[--k]);
++ free (canonical_filenames);
++ free (currdir);
++ free (git_checkout);
++ free (vc_controlled);
++ return -1;
++ }
++ canonical_filenames[n] = canonical;
++ }
++
++ /* Test which of these absolute file names are outside of the
++ git_checkout. */
++ char *git_checkout_slash =
++ (strcmp (git_checkout, "/") == 0
++ ? xstrdup (git_checkout)
++ : xasprintf ("%s/", git_checkout));
++
++ char **checkout_relative_filenames = XNMALLOC (nfiles, char *);
++ char **currdir_relative_filenames = XNMALLOC (nfiles, char *);
++ for (size_t n = 0; n < nfiles; n++)
++ {
++ if (str_startswith (canonical_filenames[n], git_checkout_slash))
++ {
++ vc_controlled[n] = 1;
++ checkout_relative_filenames[n] =
++ relativize (canonical_filenames[n],
++ 0, git_checkout, git_checkout);
++ currdir_relative_filenames[n] =
++ relativize (canonical_filenames[n],
++ ancestor, git_checkout, currdir);
++ }
++ else
++ {
++ vc_controlled[n] = 0;
++ checkout_relative_filenames[n] = NULL;
++ currdir_relative_filenames[n] = NULL;
++ }
++ }
++
++ /* Room for passing arguments to git commands. */
++ const char **argv = XNMALLOC (6 + nfiles + 1, const char *);
++
++ {
++ /* Put the relative file names into a hash table. This is needed
++ because 'git ls-files' returns the files in a different order
++ than the one we provide in the command. */
++ gl_map_t relative_filenames_ht =
++ gl_map_create_empty (GL_HASH_MAP,
++ hashkey_string_equals, hashkey_string_hash,
++ NULL, NULL);
++ for (size_t n = 0; n < nfiles; n++)
++ if (currdir_relative_filenames[n] != NULL)
++ {
++ if (gl_map_get (relative_filenames_ht, currdir_relative_filenames[n]) != NULL)
++ {
++ /* It's already in the table. */
++ vc_controlled[n] = -1;
++ }
++ else
++ gl_map_put (relative_filenames_ht, currdir_relative_filenames[n], &vc_controlled[n]);
++ }
++
++ /* Run "git ls-files -c -o -t -z FILE1..." for as many files as
++ possible, and inspect the output. */
++ size_t n0 = 0;
++ do
++ {
++ size_t i = 0;
++ argv[i++] = "git";
++ argv[i++] = "ls-files";
++ argv[i++] = "-c";
++ argv[i++] = "-o";
++ argv[i++] = "-t";
++ argv[i++] = "-z";
++ size_t i0 = i;
++
++ size_t n = n0;
++ size_t cmd_len = 25;
++ for (; n < nfiles; n++)
++ {
++ if (vc_controlled[n] == 1)
++ {
++ if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++ && i > i0)
++ break;
++ argv[i++] = currdir_relative_filenames[n];
++ cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++ }
++ n++;
++ }
++ if (i > i0)
++ {
++ pid_t child;
++ int fd[1];
++
++ argv[i] = NULL;
++ child = create_pipe_in ("git", "git", argv, NULL, NULL,
++ DEV_NULL, true, true, false, fd);
++ if (child == -1)
++ break;
++
++ /* Read the subprocess output. It is expected to be of the form
++ T1 <space> <currdir_relative_filename1> NUL
++ T2 <space> <currdir_relative_filename2> NUL
++ ...
++ where the relative filenames correspond to the given file
++ names (because we have already relativized them). */
++ FILE *fp = fdopen (fd[0], "r");
++ if (fp == NULL)
++ error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++ char *fn = NULL;
++ size_t fn_size = 0;
++ for (;;)
++ {
++ int status = fgetc (fp);
++ if (status == EOF)
++ break;
++ /* status is a status tag, as documented in
++ "man git-ls-files". */
++
++ int space = fgetc (fp);
++ if (space != ' ')
++ {
++ fprintf (stderr, "vc-mtime: git ls-files output not as expected\n");
++ break;
++ }
++
++ if (getdelim (&fn, &fn_size, '\0', fp) == -1)
++ {
++ if (errno == ENOMEM)
++ xalloc_die ();
++ fprintf (stderr, "vc-mtime: failed to read git ls-files output\n");
++ break;
++ }
++ signed char *vc_controlled_p =
++ (signed char *) gl_map_get (relative_filenames_ht, fn);
++ if (vc_controlled_p == NULL)
++ fprintf (stderr, "vc-mtime: git ls-files returned an unexpected file name: %s\n", fn);
++ else
++ *vc_controlled_p = (status == 'H' ? 1 : 0);
++ }
++
++ free (fn);
++ fclose (fp);
++
++ /* Remove zombie process from process list, and retrieve exit status. */
++ int exitstatus =
++ wait_subprocess (child, "git", false, true, true, false, NULL);
++ if (exitstatus != 0)
++ fprintf (stderr, "vc-mtime: git ls-files failed with exit code %d\n", exitstatus);
++ }
++ n0 = n;
++ }
++ while (n0 < nfiles);
++
++ gl_map_free (relative_filenames_ht);
++ }
++
++ {
++ /* Put the relative file names into a hash table. This is needed
++ because 'git diff' returns the files in a different order
++ than the one we provide in the command. */
++ gl_map_t relative_filenames_ht =
++ gl_map_create_empty (GL_HASH_MAP,
++ hashkey_string_equals, hashkey_string_hash,
++ NULL, NULL);
++ for (size_t n = 0; n < nfiles; n++)
++ if (vc_controlled[n] == 1)
++ {
++ /* No need to test for duplicates here. We have already set
++ vc_controlled[n] to -1 for duplicates, above. */
++ gl_map_put (relative_filenames_ht, checkout_relative_filenames[n], &vc_controlled[n]);
++ }
++
++ /* Run "git diff --name-only --no-relative -z HEAD -- FILE1..." for
++ as many files as possible, and inspect the output. */
++ size_t n0 = 0;
++ do
++ {
++ size_t i = 0;
++ argv[i++] = "git";
++ argv[i++] = "diff";
++ argv[i++] = "--name-only";
++ argv[i++] = "--no-relative";
++ argv[i++] = "-z";
++ argv[i++] = "HEAD";
++ argv[i++] = "--";
++ size_t i0 = i;
++
++ size_t n = n0;
++ size_t cmd_len = 46;
++ for (; n < nfiles; n++)
++ {
++ if (vc_controlled[n] == 1)
++ {
++ if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++ && i > i0)
++ break;
++ argv[i++] = currdir_relative_filenames[n];
++ cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++ }
++ n++;
++ }
++ if (i > i0)
++ {
++ pid_t child;
++ int fd[1];
++
++ argv[i] = NULL;
++ child = create_pipe_in ("git", "git", argv, NULL, NULL,
++ DEV_NULL, true, true, false, fd);
++ if (child == -1)
++ break;
++
++ /* Read the subprocess output. It is expected to be of the form
++ <checkout_relative_filename1> NUL
++ <checkout_relative_filename2> NUL
++ ...
++ where the relative filenames are relative to the git
++ checkout dir, not to currdir! */
++ FILE *fp = fdopen (fd[0], "r");
++ if (fp == NULL)
++ error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++ char *fn = NULL;
++ size_t fn_size = 0;
++ for (;;)
++ {
++ /* Test for EOF. */
++ int c = fgetc (fp);
++ if (c == EOF)
++ break;
++ ungetc (c, fp);
++
++ if (getdelim (&fn, &fn_size, '\0', fp) == -1)
++ {
++ if (errno == ENOMEM)
++ xalloc_die ();
++ fprintf (stderr, "vc-mtime: failed to read git diff output\n");
++ break;
++ }
++ signed char *vc_controlled_p =
++ (signed char *) gl_map_get (relative_filenames_ht, fn);
++ if (vc_controlled_p == NULL)
++ fprintf (stderr, "vc-mtime: git diff returned an unexpected file name: %s\n", fn);
++ else
++ /* filenames[n] is under version control but is modified.
++ Treat it like a file not under version control. */
++ *vc_controlled_p = 0;
++ }
++
++ free (fn);
++ fclose (fp);
++
++ /* Remove zombie process from process list, and retrieve exit status. */
++ int exitstatus =
++ wait_subprocess (child, "git", false, true, true, false, NULL);
++ if (exitstatus != 0)
++ fprintf (stderr, "vc-mtime: git diff failed with exit code %d\n", exitstatus);
++ }
++ n0 = n;
++ }
++ while (n0 < nfiles);
++
++ gl_map_free (relative_filenames_ht);
++ }
++
++ {
++ /* Run "git log -1 --format=%ct -- FILE1...". It prints the
++ time of last modification (the 'CommitDate', not the
++ 'AuthorDate' which merely represents the time at which the
++ author locally committed the first version of the change),
++ as the number of seconds since the Epoch. The '--' option
++ is for the case that the specified file was removed. */
++ size_t n0 = 0;
++ do
++ {
++ size_t i = 0;
++ argv[i++] = "git";
++ argv[i++] = "log";
++ argv[i++] = "-1";
++ argv[i++] = "--format=%ct";
++ argv[i++] = "--";
++ size_t i0 = i;
++
++ size_t n = n0;
++ size_t cmd_len = 27;
++ for (; n < nfiles; n++)
++ {
++ if (vc_controlled[n] == 1)
++ {
++ if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++ && i > i0)
++ break;
++ argv[i++] = currdir_relative_filenames[n];
++ cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++ }
++ n++;
++ }
++ if (i > i0)
++ {
++ pid_t child;
++ int fd[1];
++
++ argv[i] = NULL;
++ child = create_pipe_in ("git", "git", argv, NULL, NULL,
++ DEV_NULL, true, true, false, fd);
++ if (child == -1)
++ break;
++
++ /* Read the subprocess output. It is expected to be a
++ single line, containing a positive integer. */
++ FILE *fp = fdopen (fd[0], "r");
++ if (fp == NULL)
++ error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++ char *line = NULL;
++ size_t linesize = 0;
++ size_t linelen = getline (&line, &linesize, fp);
++ if (linelen == (size_t)(-1))
++ {
++ if (errno == ENOMEM)
++ xalloc_die ();
++ fprintf (stderr, "vc-mtime: failed to read git log output\n");
++ git_log_fail1:
++ free (line);
++ fclose (fp);
++ wait_subprocess (child, "git", true, false, true, false, NULL);
++ git_log_fail2:
++ free (argv);
++ for (size_t k = nfiles; k > 0; )
++ free (currdir_relative_filenames[--k]);
++ free (currdir_relative_filenames);
++ for (size_t k = nfiles; k > 0; )
++ free (checkout_relative_filenames[--k]);
++ free (checkout_relative_filenames);
++ free (git_checkout_slash);
++ for (size_t k = nfiles; k > 0; )
++ free (canonical_filenames[--k]);
++ free (canonical_filenames);
++ free (currdir);
++ free (git_checkout);
++ free (vc_controlled);
++ return -1;
++ }
++ if (linelen > 0 && line[linelen - 1] == '\n')
++ line[linelen - 1] = '\0';
++
++ char *endptr;
++ unsigned long git_log_time;
++ if (!(xstrtoul (line, &endptr, 10, &git_log_time, NULL) == LONGINT_OK
++ && endptr == line + strlen (line)))
++ {
++ fprintf (stderr, "vc-mtime: git log output not as expected\n");
++ goto git_log_fail1;
++ }
++
++ struct timespec mtime;
++ mtime.tv_sec = git_log_time;
++ mtime.tv_nsec = 0;
++ accumulate (&accu, mtime);
++
++ free (line);
++ fclose (fp);
++
++ /* Remove zombie process from process list, and retrieve exit status. */
++ int exitstatus =
++ wait_subprocess (child, "git", false, true, true, false, NULL);
++ if (exitstatus != 0)
++ {
++ fprintf (stderr, "vc-mtime: git log failed with exit code %d\n", exitstatus);
++ goto git_log_fail2;
++ }
++ }
++ n0 = n;
++ }
++ while (n0 < nfiles);
++ }
++
++ free (argv);
++ for (size_t k = nfiles; k > 0; )
++ free (currdir_relative_filenames[--k]);
++ free (currdir_relative_filenames);
++ for (size_t k = nfiles; k > 0; )
++ free (checkout_relative_filenames[--k]);
++ free (checkout_relative_filenames);
++ free (git_checkout_slash);
++ for (size_t k = nfiles; k > 0; )
++ free (canonical_filenames[--k]);
++ free (canonical_filenames);
++ }
++ free (currdir);
++ }
++ free (git_checkout);
++ }
++
++ /* For the files that are not under version control, or that are modified
++ compared to HEAD, use the file's time stamp. */
++ for (size_t n = 0; n < nfiles; n++)
++ if (vc_controlled[n] == 0)
++ {
++ struct stat statbuf;
++ if (stat (filenames[n], &statbuf) < 0)
++ {
++ free (vc_controlled);
++ return -1;
++ }
++
++ struct timespec mtime = get_stat_mtime (&statbuf);
++ accumulate (&accu, mtime);
++ }
++
++ free (vc_controlled);
++
++ /* Since nfiles > 0, we must have accumulated at least one mtime. */
++ if (!accu.has_some_mtimes)
++ abort ();
++ *max_of_mtimes = accu.max_of_mtimes;
++ return 0;
++}
++
++/* ========================================================================== */
++
++#ifdef TEST
++
++#include <assert.h>
++#include <stdio.h>
++#include <time.h>
++
++/* Some unit tests for internal functions. */
++
++static void
++test_ancestor_level (void)
++{
++ assert (ancestor_level ("/home/user/projects/gnulib", "/home/user/projects/gnulib") == 0);
++ assert (ancestor_level ("/", "/") == 0);
++
++ assert (ancestor_level ("/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto") == 2);
++ assert (ancestor_level ("/", "/home/user") == 2);
++
++ assert (ancestor_level ("/home/user/.local", "/home/user/projects/gnulib") == -1);
++ assert (ancestor_level ("/.local", "/home/user") == -1);
++ assert (ancestor_level ("/.local", "/") == -1);
++}
++
++static void
++test_relativize (void)
++{
++ assert (strcmp (relativize ("/home/user/projects/gnulib",
++ 0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"),
++ ".") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/NEWS",
++ 0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"),
++ "NEWS") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/doc/Makefile",
++ 0, "/home/user/projects/gnulib", "/home/user/projects/gnulib"),
++ "doc/Makefile") == 0);
++
++ assert (strcmp (relativize ("/",
++ 0, "/", "/"),
++ ".") == 0);
++ assert (strcmp (relativize ("/swapfile",
++ 0, "/", "/"),
++ "swapfile") == 0);
++ assert (strcmp (relativize ("/etc/passwd",
++ 0, "/", "/"),
++ "etc/passwd") == 0);
++
++ assert (strcmp (relativize ("/home/user/projects/gnulib",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ "../../") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/lib",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ "../") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/lib/crypto",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ ".") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/lib/malloc",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ "../malloc") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/lib/cr",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ "../cr") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/lib/cryptography",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ "../cryptography") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/doc",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ "../../doc") == 0);
++ assert (strcmp (relativize ("/home/user/projects/gnulib/doc/Makefile",
++ 2, "/home/user/projects/gnulib", "/home/user/projects/gnulib/lib/crypto"),
++ "../../doc/Makefile") == 0);
++
++ assert (strcmp (relativize ("/",
++ 2, "/", "/home/user"),
++ "../../") == 0);
++ assert (strcmp (relativize ("/home",
++ 2, "/", "/home/user"),
++ "../") == 0);
++ assert (strcmp (relativize ("/home/user",
++ 2, "/", "/home/user"),
++ ".") == 0);
++ assert (strcmp (relativize ("/home/root",
++ 2, "/", "/home/user"),
++ "../root") == 0);
++ assert (strcmp (relativize ("/home/us",
++ 2, "/", "/home/user"),
++ "../us") == 0);
++ assert (strcmp (relativize ("/home/users",
++ 2, "/", "/home/user"),
++ "../users") == 0);
++ assert (strcmp (relativize ("/etc",
++ 2, "/", "/home/user"),
++ "../../etc") == 0);
++ assert (strcmp (relativize ("/etc/passwd",
++ 2, "/", "/home/user"),
++ "../../etc/passwd") == 0);
++}
++
++/* Usage: ./a.out FILE[...]
++ */
++int
++main (int argc, char *argv[])
++{
++ test_ancestor_level ();
++ test_relativize ();
++
++ if (argc == 1)
++ {
++ fprintf (stderr, "Usage: ./a.out FILE[...]\n");
++ return 1;
++ }
++ struct timespec mtime;
++ int ret = max_vc_mtime (&mtime, argc - 1, (const char **) argv + 1);
++ if (ret == 0)
++ {
++ time_t t = mtime.tv_sec;
++ struct tm *gmt = gmtime (&t);
++ printf ("mtime = %04d-%02d-%02d %02d:%02d:%02d UTC\n",
++ gmt->tm_year + 1900, gmt->tm_mon + 1, gmt->tm_mday,
++ gmt->tm_hour, gmt->tm_min, gmt->tm_sec);
++ return 0;
++ }
++ else
++ {
++ printf ("failed\n");
++ return 1;
++ }
++}
++
++/*
++ * Local Variables:
++ * compile-command: "gcc -ggdb -DTEST -Wall -I. -I.. vc-mtime.c libgnu.a"
++ * End:
++ */
++
++#endif
+--- a/lib/vc-mtime.h
++++ b/lib/vc-mtime.h
+@@ -90,6 +90,13 @@ extern "C" {
+ Upon failure, it returns -1. */
+ extern int vc_mtime (struct timespec *mtime, const char *filename);
+
++/* Determines the maximum of the version-controlled modification times of
++ FILENAMES[0..NFILES-1], and returns 0.
++ Upon failure, it returns -1.
++ NFILES must be > 0. */
++extern int max_vc_mtime (struct timespec *max_of_mtimes,
++ size_t nfiles, const char * const *filenames);
++
+ #ifdef __cplusplus
+ }
+ #endif
+--- a/modules/vc-mtime
++++ b/modules/vc-mtime
+@@ -16,6 +16,17 @@ error
+ getline
+ xstrtol
+ stat-time
++filename
++xalloc
++xgetcwd
++canonicalize-lgpl
++xvasprintf
++str_startswith
++map
++xmap
++hash-map
++hashkey-string
++getdelim
+ gettext-h
+ gnulib-i18n
+
--- /dev/null
+From f4c40c2d6aabef8e587176bbf5226c8bc6649574 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Fri, 2 May 2025 02:43:23 +0200
+Subject: [PATCH] vc-mtime: Add API for more efficient use of git, part 2.
+
+* lib/vc-mtime.c (max_vc_mtime): Don't skip the odd-numbered arguments.
+---
+ ChangeLog | 5 +++++
+ lib/vc-mtime.c | 57 +++++++++++++++++++++-----------------------------
+ 2 files changed, 29 insertions(+), 33 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -558,17 +558,14 @@ max_vc_mtime (struct timespec *max_of_mt
+ size_t n = n0;
+ size_t cmd_len = 25;
+ for (; n < nfiles; n++)
+- {
+- if (vc_controlled[n] == 1)
+- {
+- if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
+- && i > i0)
+- break;
+- argv[i++] = currdir_relative_filenames[n];
+- cmd_len += 1 + strlen (currdir_relative_filenames[n]);
+- }
+- n++;
+- }
++ if (vc_controlled[n] == 1)
++ {
++ if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++ && i > i0)
++ break;
++ argv[i++] = currdir_relative_filenames[n];
++ cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++ }
+ if (i > i0)
+ {
+ pid_t child;
+@@ -672,17 +669,14 @@ max_vc_mtime (struct timespec *max_of_mt
+ size_t n = n0;
+ size_t cmd_len = 46;
+ for (; n < nfiles; n++)
+- {
+- if (vc_controlled[n] == 1)
+- {
+- if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
+- && i > i0)
+- break;
+- argv[i++] = currdir_relative_filenames[n];
+- cmd_len += 1 + strlen (currdir_relative_filenames[n]);
+- }
+- n++;
+- }
++ if (vc_controlled[n] == 1)
++ {
++ if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++ && i > i0)
++ break;
++ argv[i++] = currdir_relative_filenames[n];
++ cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++ }
+ if (i > i0)
+ {
+ pid_t child;
+@@ -768,17 +762,14 @@ max_vc_mtime (struct timespec *max_of_mt
+ size_t n = n0;
+ size_t cmd_len = 27;
+ for (; n < nfiles; n++)
+- {
+- if (vc_controlled[n] == 1)
+- {
+- if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
+- && i > i0)
+- break;
+- argv[i++] = currdir_relative_filenames[n];
+- cmd_len += 1 + strlen (currdir_relative_filenames[n]);
+- }
+- n++;
+- }
++ if (vc_controlled[n] == 1)
++ {
++ if (cmd_len + strlen (currdir_relative_filenames[n]) >= MAX_CMD_LEN
++ && i > i0)
++ break;
++ argv[i++] = currdir_relative_filenames[n];
++ cmd_len += 1 + strlen (currdir_relative_filenames[n]);
++ }
+ if (i > i0)
+ {
+ pid_t child;
--- /dev/null
+From 47548a77525a0f4489c9c420ccc2159079365da8 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Fri, 2 May 2025 12:09:40 +0200
+Subject: [PATCH] vc-mtime: Make it work with git versions < 2.28.
+
+* lib/vc-mtime.c (git_version): New variable.
+(is_git_present): Read the output of "git --version", and set
+git_version.
+(max_vc_mtime): Don't pass option --no-relative if the git version
+is < 2.28.
+---
+ ChangeLog | 9 ++++++
+ lib/vc-mtime.c | 82 +++++++++++++++++++++++++++++++++++++++++++++-----
+ 2 files changed, 83 insertions(+), 8 deletions(-)
+
+--- a/lib/vc-mtime.c
++++ b/lib/vc-mtime.c
+@@ -53,7 +53,9 @@
+
+ /* ========================================================================== */
+
+-/* Determines whether git is present. */
++static const char *git_version;
++
++/* Determines whether git is present, and sets git_version if so. */
+ static bool
+ is_git_present (void)
+ {
+@@ -63,17 +65,67 @@ is_git_present (void)
+ if (!git_tested)
+ {
+ /* Test for presence of git:
+- "git --version >/dev/null 2>/dev/null" */
++ "git --version 2>/dev/null" */
+ const char *argv[3];
+- int exitstatus;
++ pid_t child;
++ int fd[1];
+
+ argv[0] = "git";
+ argv[1] = "--version";
+ argv[2] = NULL;
+- exitstatus = execute ("git", "git", argv, NULL, NULL,
+- false, false, true, true,
+- true, false, NULL);
+- git_present = (exitstatus == 0);
++ child = create_pipe_in ("git", "git", argv, NULL, NULL,
++ DEV_NULL, true, true, false, fd);
++ if (child == -1)
++ git_present = false;
++ else
++ {
++ /* Retrieve its result. */
++ FILE *fp = fdopen (fd[0], "r");
++ if (fp == NULL)
++ error (EXIT_FAILURE, errno, _("fdopen() failed"));
++
++ char *line = NULL;
++ size_t linesize = 0;
++ size_t linelen = getline (&line, &linesize, fp);
++ if (linelen == (size_t)(-1))
++ {
++ fclose (fp);
++ wait_subprocess (child, "git", true, true, true, false, NULL);
++ git_present = false;
++ }
++ else
++ {
++ if (linelen > 0 && line[linelen - 1] == '\n')
++ line[linelen - 1] = '\0';
++
++ /* Read until EOF (otherwise the child process may get a SIGPIPE
++ signal). */
++ while (getc (fp) != EOF)
++ ;
++
++ fclose (fp);
++
++ /* Remove zombie process from process list, and retrieve exit
++ status. */
++ int exitstatus =
++ wait_subprocess (child, "git", true, true, true, false, NULL);
++ if (exitstatus != 0)
++ {
++ free (line);
++ git_present = false;
++ }
++ else
++ {
++ /* The version starts at the first digit in the line. */
++ const char *p = line;
++ for (; *p != '0'; p++)
++ if (*p >= '0' && *p <= '9')
++ break;
++ git_version = p;
++ git_present = true;
++ }
++ }
++ }
+ git_tested = true;
+ }
+
+@@ -660,7 +712,21 @@ max_vc_mtime (struct timespec *max_of_mt
+ argv[i++] = "git";
+ argv[i++] = "diff";
+ argv[i++] = "--name-only";
+- argv[i++] = "--no-relative";
++ /* With git versions >= 2.28, we pass option --no-relative,
++ in order to neutralize any possible customization of the
++ "diff.relative" property. With git versions < 2.28, this
++ is not needed, and the option --no-relative does not
++ exist. */
++ if (!(git_version[0] <= '1'
++ || (git_version[0] == '2' && git_version[1] == '.'
++ && ((git_version[2] >= '0' && git_version[2] <= '9'
++ && !(git_version[3] >= '0' && git_version[3] <= '9'))
++ || (((git_version[2] == '1'
++ && git_version[3] >= '0' && git_version[3] <= '9')
++ || (git_version[2] == '2'
++ && git_version[3] >= '0' && git_version[3] <= '7'))
++ && !(git_version[4] >= '0' && git_version[4] <= '9'))))))
++ argv[i++] = "--no-relative";
+ argv[i++] = "-z";
+ argv[i++] = "HEAD";
+ argv[i++] = "--";
--- /dev/null
+From 24010120fab36721caaf92be076655571e44da07 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Fri, 3 Jan 2025 09:26:14 +0100
+Subject: [PATCH] str_startswith: New module.
+
+* lib/string.in.h (str_startswith): New declaration.
+* lib/str_startswith.c: New file.
+* m4/string_h.m4 (gl_STRING_H_REQUIRE_DEFAULTS): Initialize
+GNULIB_STR_STARTSWITH.
+* modules/string-h (Makefile.am): Substitute GNULIB_STR_STARTSWITH.
+* modules/str_startswith: New file.
+---
+ ChangeLog | 10 ++++++++++
+ lib/str_startswith.c | 29 +++++++++++++++++++++++++++++
+ lib/string.in.h | 8 ++++++++
+ m4/string_h.m4 | 3 ++-
+ modules/str_startswith | 23 +++++++++++++++++++++++
+ modules/string-h | 1 +
+ 6 files changed, 73 insertions(+), 1 deletion(-)
+ create mode 100644 lib/str_startswith.c
+ create mode 100644 modules/str_startswith
+
+--- /dev/null
++++ b/lib/str_startswith.c
+@@ -0,0 +1,29 @@
++/* str_startswith function.
++ Copyright (C) 2025 Free Software Foundation, Inc.
++
++ This file 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 3 of the
++ License, or (at your option) any later version.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
++
++#include "config.h"
++
++/* Specification. */
++#include <string.h>
++
++
++int
++str_startswith (const char *string, const char *prefix)
++{
++ return strncmp (string, prefix, strlen (prefix)) == 0;
++}
+--- a/lib/string.in.h
++++ b/lib/string.in.h
+@@ -1079,6 +1079,14 @@ _GL_WARN_ON_USE (strtok_r, "strtok_r is
+ /* The following functions are not specified by POSIX. They are gnulib
+ extensions. */
+
++#if @GNULIB_STR_STARTSWITH@
++/* Returns true if STRING starts with PREFIX.
++ Returns false otherwise. */
++_GL_EXTERN_C int str_startswith (const char *string, const char *prefix)
++ _GL_ATTRIBUTE_PURE
++ _GL_ARG_NONNULL ((1, 2));
++#endif
++
+ #if @GNULIB_MBSLEN@
+ /* Return the number of multibyte characters in the character string STRING.
+ This considers multibyte characters, unlike strlen, which counts bytes. */
+--- a/m4/string_h.m4
++++ b/m4/string_h.m4
+@@ -70,6 +70,7 @@ AC_DEFUN([gl_STRING_H_REQUIRE_DEFAULTS],
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRSTR])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRCASESTR])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRTOK_R])
++ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STR_STARTSWITH])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSLEN])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSNLEN])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSCHR])
+--- /dev/null
++++ b/modules/str_startswith
+@@ -0,0 +1,23 @@
++Description:
++str_startswith() function: test whether a string starts with a given prefix.
++
++Files:
++lib/str_startswith.c
++
++Depends-on:
++string-h
++
++configure.ac:
++gl_STRING_MODULE_INDICATOR([str_startswith])
++
++Makefile.am:
++lib_SOURCES += str_startswith.c
++
++Include:
++<string.h>
++
++License:
++LGPLv2+
++
++Maintainer:
++all
+--- a/modules/string-h
++++ b/modules/string-h
+@@ -69,6 +69,7 @@ string.h: string.in.h $(top_builddir)/co
+ -e 's/@''GNULIB_STRSTR''@/$(GNULIB_STRSTR)/g' \
+ -e 's/@''GNULIB_STRCASESTR''@/$(GNULIB_STRCASESTR)/g' \
+ -e 's/@''GNULIB_STRTOK_R''@/$(GNULIB_STRTOK_R)/g' \
++ -e 's/@''GNULIB_STR_STARTSWITH''@/$(GNULIB_STR_STARTSWITH)/g' \
+ -e 's/@''GNULIB_STRERROR''@/$(GNULIB_STRERROR)/g' \
+ -e 's/@''GNULIB_STRERROR_R''@/$(GNULIB_STRERROR_R)/g' \
+ -e 's/@''GNULIB_STRERRORNAME_NP''@/$(GNULIB_STRERRORNAME_NP)/g' \
--- /dev/null
+From d89ac9373d9748f7601babf52c9129fcbcf0c907 Mon Sep 17 00:00:00 2001
+From: Bruno Haible <bruno@clisp.org>
+Date: Fri, 3 Jan 2025 09:54:14 +0100
+Subject: [PATCH] str_endswith: New module.
+
+* lib/string.in.h (str_endswith): New declaration.
+* lib/str_endswith.c: New file.
+* m4/string_h.m4 (gl_STRING_H_REQUIRE_DEFAULTS): Initialize
+GNULIB_STR_ENDSWITH.
+* modules/string-h (Makefile.am): Substitute GNULIB_STR_ENDSWITH.
+* modules/str_endswith: New file.
+---
+ ChangeLog | 10 ++++++++++
+ lib/str_endswith.c | 31 +++++++++++++++++++++++++++++++
+ lib/string.in.h | 8 ++++++++
+ m4/string_h.m4 | 3 ++-
+ modules/str_endswith | 23 +++++++++++++++++++++++
+ modules/string-h | 1 +
+ 6 files changed, 75 insertions(+), 1 deletion(-)
+ create mode 100644 lib/str_endswith.c
+ create mode 100644 modules/str_endswith
+
+--- /dev/null
++++ b/lib/str_endswith.c
+@@ -0,0 +1,31 @@
++/* str_endswith function.
++ Copyright (C) 2025 Free Software Foundation, Inc.
++
++ This file 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 3 of the
++ License, or (at your option) any later version.
++
++ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
++
++/* Written by Bruno Haible <bruno@clisp.org>, 2025. */
++
++#include "config.h"
++
++/* Specification. */
++#include <string.h>
++
++
++int
++str_endswith (const char *string, const char *suffix)
++{
++ size_t len = strlen (string);
++ size_t n = strlen (suffix);
++ return len >= n && strcmp (string + len - n, suffix) == 0;
++}
+--- a/lib/string.in.h
++++ b/lib/string.in.h
+@@ -1087,6 +1087,14 @@ _GL_EXTERN_C int str_startswith (const c
+ _GL_ARG_NONNULL ((1, 2));
+ #endif
+
++#if @GNULIB_STR_ENDSWITH@
++/* Returns true if STRING ends with SUFFIX.
++ Returns false otherwise. */
++_GL_EXTERN_C int str_endswith (const char *string, const char *prefix)
++ _GL_ATTRIBUTE_PURE
++ _GL_ARG_NONNULL ((1, 2));
++#endif
++
+ #if @GNULIB_MBSLEN@
+ /* Return the number of multibyte characters in the character string STRING.
+ This considers multibyte characters, unlike strlen, which counts bytes. */
+--- a/m4/string_h.m4
++++ b/m4/string_h.m4
+@@ -71,6 +71,7 @@ AC_DEFUN([gl_STRING_H_REQUIRE_DEFAULTS],
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRCASESTR])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STRTOK_R])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STR_STARTSWITH])
++ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_STR_ENDSWITH])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSLEN])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSNLEN])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MBSCHR])
+--- /dev/null
++++ b/modules/str_endswith
+@@ -0,0 +1,23 @@
++Description:
++str_endswith() function: test whether a string ends with a given suffix.
++
++Files:
++lib/str_endswith.c
++
++Depends-on:
++string-h
++
++configure.ac:
++gl_STRING_MODULE_INDICATOR([str_endswith])
++
++Makefile.am:
++lib_SOURCES += str_endswith.c
++
++Include:
++<string.h>
++
++License:
++LGPLv2+
++
++Maintainer:
++all
+--- a/modules/string-h
++++ b/modules/string-h
+@@ -69,6 +69,7 @@ string.h: string.in.h $(top_builddir)/co
+ -e 's/@''GNULIB_STRSTR''@/$(GNULIB_STRSTR)/g' \
+ -e 's/@''GNULIB_STRCASESTR''@/$(GNULIB_STRCASESTR)/g' \
+ -e 's/@''GNULIB_STRTOK_R''@/$(GNULIB_STRTOK_R)/g' \
++ -e 's/@''GNULIB_STR_ENDSWITH''@/$(GNULIB_STR_ENDSWITH)/g' \
+ -e 's/@''GNULIB_STR_STARTSWITH''@/$(GNULIB_STR_STARTSWITH)/g' \
+ -e 's/@''GNULIB_STRERROR''@/$(GNULIB_STRERROR)/g' \
+ -e 's/@''GNULIB_STRERROR_R''@/$(GNULIB_STRERROR_R)/g' \