commit e5dc09c4c1c67b426b2dc87390e7f51d8f707008
parent f1cac449f1fb07c9f386e5be8903b61520fde283
Author: Evgeny Grin (Karlson2k) <k2k@drgrin.dev>
Date: Sun, 19 Jul 2026 12:43:31 +0200
Implemented portable code for temporary files for the test suite
Diffstat:
5 files changed, 912 insertions(+), 1 deletion(-)
diff --git a/configure.ac b/configure.ac
@@ -8364,6 +8364,49 @@ AC_LINK_IFELSE(
AM_CONDITIONAL([HAVE_FORK_WAITPID], [test "x$mhd_have_fork_waitpid" = "xyes"])
+
+# Other stuff used in testing
+
+MHD_CHECK_FUNC([mkostemp],
+ [[
+#if defined(HAVE_STDLIB_H)
+# include <stdlib.h>
+#endif
+#if defined(HAVE_UNISTD_H)
+/* On Darwin mkostemp() is declared in unistd.h */
+# include <unistd.h>
+#endif
+#include <fcntl.h>
+ ]],
+ [[
+ char tmp_fname[] = "/tmp/mhd-test-XXXXXX";
+ int tmp_fd = mkostemp(tmp_fname, 0);
+ i][f (0 > tmp_fd)
+ return 3;
+ close(tmp_fd);
+ ]],
+ [],
+ [
+ MHD_CHECK_FUNC([mkstemp],
+ [[
+#if defined(HAVE_STDLIB_H)
+# include <stdlib.h>
+#endif
+#if defined(HAVE_UNISTD_H)
+# include <unistd.h>
+#endif
+ ]],
+ [[
+ char tmp_fname[] = "/tmp/mhd-test-XXXXXX";
+ int tmp_fd = mkstemp(tmp_fname);
+ i][f (0 > tmp_fd)
+ return 3;
+ close(tmp_fd);
+ ]]
+ )
+ ]
+)
+
# gcov compilation
AC_MSG_CHECKING(whether to compile with support for code coverage analysis)
AC_ARG_ENABLE([coverage],
diff --git a/src/tests/.gitignore b/src/tests/.gitignore
@@ -1,3 +1,4 @@
test_*[a-z0-9_][a-z0-9_][a-z0-9_]
!*.c
!*.h
+mhdt-tmp-*
diff --git a/src/tests/client_server/Makefile.am b/src/tests/client_server/Makefile.am
@@ -56,7 +56,8 @@ libmhdt_la_SOURCES = \
libtest.c libtest.h \
libtest_convenience.c \
libtest_convenience_client_request.c \
- libtest_convenience_server_reply.c
+ libtest_convenience_server_reply.c \
+ mhdt_tmpfile.c mhdt_tmpfile.h
# TODO: fix out-of-tree 'make check'
EXTRA_DIST = \
diff --git a/src/tests/client_server/mhdt_tmpfile.c b/src/tests/client_server/mhdt_tmpfile.c
@@ -0,0 +1,796 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
+/*
+ This file is part of GNU libmicrohttpd.
+ Copyright (C) 2026 Evgeny Grin (Karlson2k)
+
+ GNU libmicrohttpd 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.
+
+ GNU libmicrohttpd 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.
+
+ Alternatively, you can redistribute GNU libmicrohttpd and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of
+ the License, or (at your option) any later version, together
+ with the eCos exception, as follows:
+
+ As a special exception, if other files instantiate templates or
+ use macros or inline functions from this file, or you compile this
+ file and link it with other works to produce a work based on this
+ file, this file does not by itself cause the resulting work to be
+ covered by the GNU General Public License. However the source code
+ for this file must still be made available in accordance with
+ section (3) of the GNU General Public License v2.
+
+ This exception does not invalidate any other reasons why a work
+ based on this file might be covered by the GNU General Public
+ License.
+
+ You should have received copies of the GNU Lesser General Public
+ License and the GNU General Public License along with this library;
+ if not, see <https://www.gnu.org/licenses/>.
+*/
+
+/**
+ * @file src/tests/client_server/mhdt_tmpfile.c
+ * @brief MHD test framework temporary file helpers
+ * @author Karlson2k (Evgeny Grin)
+ */
+
+#include "mhd_sys_options.h"
+
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#if !defined(_WIN32) || defined(__CYGWIN__)
+# ifdef HAVE_UNISTD_H
+# include <unistd.h>
+# endif
+# include <stdio.h> /* for P_tmpdir */
+#else
+# define mhdt_USE_W32 1
+# ifndef WIN32_LEAN_AND_MEAN
+# define WIN32_LEAN_AND_MEAN 1
+# endif
+# include <windows.h>
+# include <wchar.h>
+# include <sys/types.h>
+# include <sys/stat.h>
+# include <io.h>
+#endif
+
+#include "mhdt_tmpfile.h"
+
+#ifndef mhdt_ARR_CNT
+# define mhdt_ARR_CNT(arr) (sizeof(arr)/sizeof(arr[0]))
+#endif
+
+#ifndef MHDT_TMP_FILE_BASENAME_PREFIX
+# define MHDT_TMP_FILE_BASENAME_PREFIX "mhdt-tmp-"
+#endif
+
+/* The length of the unique file suffix */
+#ifndef MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN
+# define MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN 6
+#endif
+
+#if MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN < 6
+# error MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN is less than 6
+#endif
+
+/* Maximum length of a normalised temporary directory name,
+ including the trailing slash but excluding the terminating NUL. */
+#if !defined(MHDT_FILENAME_MAX)
+# if defined(FILENAME_MAX)
+# define MHDT_FILENAME_MAX FILENAME_MAX
+# elif defined(MAX_PATH)
+# define MHDT_FILENAME_MAX MAX_PATH
+# endif
+#endif
+
+#if defined(MHDT_FILENAME_MAX) && (MHDT_FILENAME_MAX + 0) > 4096
+# undef MHDT_FILENAME_MAX
+#endif
+
+#ifndef MHDT_FILENAME_MAX
+# define MHDT_FILENAME_MAX 4096
+#endif
+
+#ifndef mhdt_MAX_TRIES
+# define mhdt_MAX_TRIES 127
+#endif
+
+#define MHDTL_UINT32_MASK 0xFFFFFFFFu
+
+
+static inline uint_least32_t
+mhdtl_rotl32 (uint_least32_t val,
+ unsigned int rot)
+{
+ uint_fast32_t v;
+
+ rot &= 31u;
+ v = ((uint_fast32_t)val) & MHDTL_UINT32_MASK;
+
+ return (uint_least32_t)
+ (((v << rot)
+ | (v >> ((32u - rot) & 31u)))
+ & MHDTL_UINT32_MASK);
+}
+
+
+/**
+ * Mix bits of two 32-bit values and produce a pseudo-random 32-bit value
+ * based on them.
+ * @param a the first value to mix
+ * @param b the second value to mix
+ * @return the pseudo-random value produced from the input values
+ */
+static inline uint_least32_t
+mhdtl_mix32 (uint_fast32_t a,
+ uint_fast32_t b)
+{
+ uint_fast32_t res;
+
+ /* Combine the inputs: multiplication by the Golden Ratio's fractional
+ * part diffuses the bits of 'a' before they meet the bits of 'b',
+ * while the rotation breaks the symmetry between the two inputs. */
+ res =
+ ((((a & MHDTL_UINT32_MASK) * 0x9E3779B9u) & MHDTL_UINT32_MASK)
+ ^ mhdtl_rotl32 ((uint_least32_t)b, 13u));
+ /* Re-mix bits better.
+ * The shifts and the multipliers are the MurmurHash3 32-bit finaliser. */
+ res ^= res >> 16u;
+ res = (res * 0x85EBCA6Bu) & MHDTL_UINT32_MASK;
+ res ^= res >> 13u;
+ res = (res * 0xC2B2AE35u) & MHDTL_UINT32_MASK;
+ res ^= res >> 16u;
+
+ return (uint_least32_t)res;
+}
+
+
+#ifndef mhdt_USE_W32
+
+/**
+ * Try to add the specified directory to the list of the directories used
+ * for temporary files.
+ *
+ * The candidate name is normalised before use: a trailing slash is appended
+ * if the name does not end with a slash already.
+ * The candidate is silently ignored if the name is NULL or empty, if
+ * the normalised name is too long to fit into the list entry, or if
+ * the normalised name duplicates any name already present in the list.
+ * The @p tmp_dirs array must have a free entry at the index
+ * @p *dirs_have_ptr.
+ *
+ * @param tmp_dir_name the name of the candidate directory, can be NULL
+ * @param[in,out] tmp_dirs the array of the names of the directories used
+ * for temporary files
+ * @param[in,out] dirs_have_ptr the pointer to the variable with the number
+ * of the used entries in the @p tmp_dirs array,
+ * the value is incremented if the candidate
+ * is added
+ */
+static void
+tmpdirs_try_add (const char *tmp_dir_name,
+ char (*tmp_dirs)[MHDT_FILENAME_MAX + 1],
+ unsigned int *dirs_have_ptr)
+{
+ unsigned int i;
+ char tmp_dir[MHDT_FILENAME_MAX + 1];
+ size_t tmp_dir_name_len;
+
+ if (NULL == tmp_dir_name)
+ return;
+ tmp_dir_name_len = strlen (tmp_dir_name);
+ if (0u == tmp_dir_name_len)
+ return;
+ if (((size_t)MHDT_FILENAME_MAX) < tmp_dir_name_len)
+ return; /* String 'tmp_dir_name' is too long to use */
+
+ memcpy (tmp_dir,
+ tmp_dir_name,
+ tmp_dir_name_len + 1);
+
+ if ('/' != tmp_dir[tmp_dir_name_len - 1u])
+ {
+ if (((size_t)MHDT_FILENAME_MAX) == tmp_dir_name_len)
+ return; /* String 'tmp_dir_name' is too long to use */
+ tmp_dir[tmp_dir_name_len++] = '/';
+ tmp_dir[tmp_dir_name_len] = '\0';
+ }
+
+ for (i = 0; i < *dirs_have_ptr; ++i)
+ if (0 == strcmp (tmp_dir, tmp_dirs[i]))
+ return; /* Found duplicate */
+
+ memcpy (tmp_dirs[*dirs_have_ptr], tmp_dir, tmp_dir_name_len + 1u);
+ ++(*dirs_have_ptr);
+}
+
+
+/**
+ * Fill the buffer with the hexadecimal representation of the number.
+ *
+ * Exactly @p buf_size - 1 lower hexadecimal digits of the @p number are
+ * written (zero-padded), followed by the terminating NUL.
+ * @param buf_size the size of the @p buf buffer in characters,
+ * must not be zero
+ * @param[out] buf the buffer to fill
+ * @param number the number to convert
+ */
+static void
+mhdt_num_to_hex (size_t buf_size,
+ char *buf,
+ uint_least32_t number)
+{
+ static const char hex_map[] = "0123456789abcdef";
+ size_t p = buf_size - 1u;
+ if (p > buf_size)
+ abort ();
+ buf[p] = '\0';
+ while (--p < buf_size)
+ {
+ buf[p] = hex_map[number & 0xFu];
+ number >>= 4u;
+ }
+}
+
+
+/**
+ * Get a directory for temporary files
+ * @param fallback_num the number of the fallback location,
+ * zero for the main location
+ * @return the temporary directory string (always ends with a slash, the length
+ * of the string is not greater than #MHDT_FILENAME_MAX),
+ * NULL if @p fallback_num is too high
+ */
+static const char *
+mhdt_get_tmpfile_dir (unsigned int fallback_num)
+{
+ /* 'volatile' does not provide thread-safety, but the stored values are
+ identical for every initialisation.
+ This simplification is considered acceptable for the testing code. */
+ static volatile unsigned int dirs_have = 0u;
+ static char tmp_dirs[7][MHDT_FILENAME_MAX + 1] =
+ { "", "", "", "", "", "", "" };
+
+ if (0u == dirs_have)
+ {
+ unsigned int dirs_found = 0u;
+
+ tmpdirs_try_add (getenv ("TMPDIR"), tmp_dirs, &dirs_found);
+
+ tmpdirs_try_add (getenv ("TMP"), tmp_dirs, &dirs_found);
+
+ tmpdirs_try_add (getenv ("TEMP"), tmp_dirs, &dirs_found);
+
+ tmpdirs_try_add (".", tmp_dirs, &dirs_found);
+
+# ifdef P_tmpdir
+ tmpdirs_try_add (P_tmpdir, tmp_dirs, &dirs_found);
+# endif
+
+ tmpdirs_try_add ("/tmp", tmp_dirs, &dirs_found);
+
+ if (sizeof(tmp_dirs) / sizeof(tmp_dirs[0]) < dirs_found)
+ abort ();
+
+ if (0u == dirs_found)
+ abort ();
+
+ dirs_have = dirs_found;
+ }
+
+ if (dirs_have <= fallback_num)
+ return NULL;
+
+ return tmp_dirs[fallback_num];
+}
+
+
+/**
+ * Create a temporary file that is deleted automatically when the FD is
+ * closed.
+ *
+ * The file is opened for both reading and writing.
+ * The close-on-exec flag is set for the FD (if supported by the platform).
+ *
+ * The locations for temporary files are tried in the following order:
+ * + the directories set by the "TMPDIR", the "TMP" and the "TEMP"
+ * environment variables,
+ * + the current directory,
+ * + the directory specified by the P_tmpdir macro (if defined),
+ * + and the "/tmp" directory.
+ * The locations that are unset, too long or duplicated are skipped.
+ *
+ * @return the FD of the created file on success,
+ * -1 if the file cannot be created in any of the locations
+ */
+static int
+mhdt_create_tmpfile_posix (void)
+{
+ static const char basename_prfx[] = MHDT_TMP_FILE_BASENAME_PREFIX;
+ const char *tmp_dir;
+ unsigned int i;
+
+ for (i = 0u; NULL != (tmp_dir = mhdt_get_tmpfile_dir (i)); ++i)
+ {
+ char tmp_filename[MHDT_FILENAME_MAX
+ + mhdt_ARR_CNT (basename_prfx)
+ + MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN];
+ int tmpfile_fd;
+ int need_cloexec;
+ int need_unlink;
+ size_t tmp_dir_len;
+
+ tmp_dir_len = strlen (tmp_dir);
+ memcpy (tmp_filename, tmp_dir, tmp_dir_len);
+ memcpy (tmp_filename + tmp_dir_len,
+ basename_prfx,
+ mhdt_ARR_CNT (basename_prfx) - 1u);
+ memset (tmp_filename + tmp_dir_len + mhdt_ARR_CNT (basename_prfx) - 1u,
+ 'X',
+ MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN);
+ tmp_filename[tmp_dir_len + mhdt_ARR_CNT (basename_prfx) - 1u
+ + MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN] = '\0';
+
+ tmpfile_fd = -1;
+ need_cloexec = !0;
+ need_unlink = !0;
+
+# ifdef O_TMPFILE
+ if (0 > tmpfile_fd)
+ {
+ int flags;
+ flags = O_RDWR | O_TMPFILE;
+# ifdef O_EXCL
+ flags |= O_EXCL;
+# else
+ /* The missing O_EXCL support cannot be replaced with anything.
+ Open file without O_EXCL as this is only the testing code. */
+# endif
+# ifdef O_CLOEXEC
+ flags |= O_CLOEXEC;
+# endif
+# if defined(O_CLOFORK) && !defined(__linux__)
+ flags |= O_CLOFORK;
+# endif
+
+ tmpfile_fd = open (tmp_dir,
+ flags,
+ 0600);
+ if (0 <= tmpfile_fd)
+ {
+ need_unlink = 0;
+# ifdef O_CLOEXEC
+ need_cloexec = 0;
+# endif
+ }
+ }
+# endif
+
+# ifdef HAVE_MKOSTEMP
+ if (0 > tmpfile_fd)
+ {
+ int flags;
+ flags = 0;
+
+# ifdef O_CLOEXEC
+ flags |= O_CLOEXEC;
+# endif
+# if defined(O_CLOFORK) && !defined(__linux__)
+ flags |= O_CLOFORK;
+# endif
+ tmpfile_fd = mkostemp (tmp_filename,
+ flags);
+# ifdef O_CLOEXEC
+ if (0 <= tmpfile_fd)
+ need_cloexec = 0;
+# endif
+ }
+# elif defined(HAVE_MKSTEMP)
+ if (0 > tmpfile_fd)
+ tmpfile_fd = mkstemp (tmp_filename);
+# endif
+
+ /* Last resort */
+ if (0 > tmpfile_fd)
+ {
+ static volatile unsigned int cnt = 0u;
+ int flags;
+ int t;
+ uint_least32_t suff_num;
+
+# if defined(HAVE_MKOSTEMP) || defined(HAVE_MKSTEMP)
+ /* Restore the filename template in case it was broken */
+ memcpy (tmp_filename, tmp_dir, tmp_dir_len);
+ memcpy (tmp_filename + tmp_dir_len,
+ basename_prfx,
+ mhdt_ARR_CNT (basename_prfx) - 1u);
+# endif /* HAVE_MKOSTEMP || HAVE_MKSTEMP */
+
+ flags = O_CREAT | O_RDWR;
+ /* Last resort: do not try O_CLOFORK as it may be unsupported */
+# ifdef O_EXCL
+ flags |= O_EXCL;
+# else
+ /* The missing O_EXCL support cannot be replaced with anything.
+ Open file without O_EXCL as this is only the testing code. */
+# endif
+# ifdef O_CLOEXEC
+ flags |= O_CLOEXEC;
+# endif
+ suff_num = mhdtl_mix32 ((uint_least32_t)getpid (),
+ (uint_least32_t)mhd_PTR_TO_INT (tmp_filename));
+ for (t = 0; mhdt_MAX_TRIES > t; ++t)
+ {
+ char *suff_ptr;
+ suff_num = mhdtl_mix32 (suff_num, (uint_least32_t)cnt++);
+ suff_ptr =
+ tmp_filename + tmp_dir_len + mhdt_ARR_CNT (basename_prfx) - 1u;
+
+ mhdt_num_to_hex (MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN + 1u,
+ suff_ptr,
+ suff_num);
+
+ tmpfile_fd = open (tmp_filename,
+ flags,
+ 0600);
+ if (0 <= tmpfile_fd)
+ {
+# ifdef O_CLOEXEC
+ need_cloexec = 0;
+# endif
+ break;
+ }
+ else if ((EEXIST != errno) && (EINTR != errno))
+ break; /* Avoid useless retries */
+ }
+ }
+
+ if (0 > tmpfile_fd)
+ continue; /* Try next directory */
+
+ /* Best effort to set CLOEXEC */
+# ifdef FD_CLOEXEC
+ if (need_cloexec)
+ {
+ int flags;
+
+ flags = fcntl (tmpfile_fd,
+ F_GETFD);
+ if (0 <= flags)
+ {
+ flags |= FD_CLOEXEC;
+# if defined(FD_CLOFORK)
+ if (0 == fcntl (tmpfile_fd,
+ F_SETFD,
+ flags | FD_CLOFORK))
+ (void)0; /* Success. Do nothing. */
+ else
+# endif
+ if (!0)
+ { /* FD_CLOFORK may be unsupported */
+ int res;
+ res = fcntl (tmpfile_fd,
+ F_SETFD,
+ flags);
+ (void)res; /* Ignore result */
+ }
+ }
+ }
+# else
+ (void)need_cloexec; /* Use without CLOEXEC */
+# endif
+
+ if (need_unlink)
+ {
+ if (0 != unlink (tmp_filename))
+ {
+ int res;
+ /* The opened file cannot be unlinked. */
+ res = close (tmpfile_fd);
+ (void)res; /* Ignore result */
+ res = unlink (tmp_filename); /* Retry the removal with closed FD */
+ (void)res; /* Ignore result */
+ continue; /* Try next directory */
+ }
+ }
+
+ return tmpfile_fd;
+ }
+
+ return -1;
+}
+
+
+#else /* W32 */
+
+/**
+ * Add a temporary-directory candidate to the list.
+ *
+ * The candidate is normalised in place to end with a backslash. It is ignored
+ * if NULL, empty, too long, or an exact duplicate of an existing entry.
+ * @param tmp_dir_name_len length of @p tmp_dir_name, excluding the NUL
+ * @param[in,out] tmp_dir_name writable NUL-terminated buffer of
+ * #MHDT_FILENAME_MAX + 1 characters
+ * @param[in,out] tmp_dirs destination array with a free entry at
+ * @p *dirs_have_ptr
+ * @param[in,out] dirs_have_ptr number of used entries, incremented on addition
+ */
+static void
+tmpdirs_try_add_w32 (unsigned long tmp_dir_name_len,
+ wchar_t tmp_dir_name[MHDT_FILENAME_MAX + 1],
+ wchar_t (*tmp_dirs)[MHDT_FILENAME_MAX + 1],
+ unsigned int *dirs_have_ptr)
+{
+ unsigned int i;
+ size_t tmp_dir_name_len_sz;
+
+ if (NULL == tmp_dir_name)
+ return;
+ if (0u == tmp_dir_name_len)
+ return;
+ if (((unsigned long)MHDT_FILENAME_MAX) < tmp_dir_name_len)
+ return; /* String 'tmp_dir_name' cannot fit the buffer */
+ tmp_dir_name_len_sz = (size_t)tmp_dir_name_len;
+
+
+ if (L'/' == tmp_dir_name[tmp_dir_name_len_sz - 1u])
+ tmp_dir_name[tmp_dir_name_len_sz - 1u] = L'\\';
+
+ if (L'\\' != tmp_dir_name[tmp_dir_name_len_sz - 1u])
+ {
+ if (((size_t)MHDT_FILENAME_MAX) == tmp_dir_name_len_sz)
+ return; /* String 'tmp_dir_name' is too long to use */
+ /* This converts a drive-relative path "C:" to the root dir "C:\" which is
+ the desired effect (a drive-relative path is a bad choice). */
+ tmp_dir_name[tmp_dir_name_len_sz++] = L'\\';
+ tmp_dir_name[tmp_dir_name_len_sz] = L'\0';
+ }
+
+ /* Simplified naive deduplication: not all slashes are normalised and
+ characters are not case-folded.
+ The filesystems on W32 can be case-sensitive (even with a per-directory
+ attribute). */
+ for (i = 0; i < *dirs_have_ptr; ++i)
+ if (0 == wcscmp (tmp_dir_name, tmp_dirs[i]))
+ return; /* Found duplicate */
+
+ wmemcpy (tmp_dirs[*dirs_have_ptr],
+ tmp_dir_name,
+ tmp_dir_name_len_sz + 1u);
+ ++(*dirs_have_ptr);
+}
+
+
+/**
+ * Get a directory for temporary files
+ * @param fallback_num the number of the fallback location,
+ * zero for the main location
+ * @return the temporary directory string (always ends with a backslash,
+ * the length of the string is not greater than #MHDT_FILENAME_MAX),
+ * NULL if @p fallback_num is too high
+ */
+static const wchar_t *
+mhdt_get_tmpfile_dir_w32 (unsigned int fallback_num)
+{
+ /* 'volatile' does not provide thread-safety, but the stored values are
+ identical for every initialisation.
+ This simplification is considered acceptable for the testing code. */
+ static volatile unsigned int dirs_have = 0u;
+ static wchar_t tmp_dirs[6][MHDT_FILENAME_MAX + 1] =
+ { L"", L"", L"", L"", L"", L"" };
+ wchar_t var_buf[MHDT_FILENAME_MAX + 1];
+ const unsigned int var_buf_size = (sizeof(var_buf) / sizeof(var_buf[0]));
+
+ if (0u == dirs_have)
+ {
+ unsigned int dirs_found = 0u;
+
+ tmpdirs_try_add_w32 (GetEnvironmentVariableW (L"TMP",
+ var_buf,
+ var_buf_size),
+ var_buf,
+ tmp_dirs,
+ &dirs_found);
+
+ tmpdirs_try_add_w32 (GetEnvironmentVariableW (L"TEMP",
+ var_buf,
+ var_buf_size),
+ var_buf,
+ tmp_dirs,
+ &dirs_found);
+
+ wmemcpy (var_buf, L".\\", 2u + 1u);
+ tmpdirs_try_add_w32 (2u,
+ var_buf,
+ tmp_dirs,
+ &dirs_found);
+
+ tmpdirs_try_add_w32 (GetEnvironmentVariableW (L"USERPROFILE",
+ var_buf,
+ var_buf_size),
+ var_buf,
+ tmp_dirs,
+ &dirs_found);
+
+ /* This is mostly a duplication of one of the previous values */
+ tmpdirs_try_add_w32 (GetTempPathW (var_buf_size,
+ var_buf),
+ var_buf,
+ tmp_dirs,
+ &dirs_found);
+
+ if (sizeof(tmp_dirs) / sizeof(tmp_dirs[0]) < dirs_found)
+ abort ();
+
+ if (0u == dirs_found)
+ abort ();
+
+ dirs_have = dirs_found;
+ }
+
+ if (dirs_have <= fallback_num)
+ return NULL;
+
+ return tmp_dirs[fallback_num];
+}
+
+
+/**
+ * Fill the buffer with the hexadecimal representation of the number.
+ *
+ * Exactly @p buf_size - 1 lower hexadecimal digits of the @p number are
+ * written (zero-padded), followed by the terminating NUL.
+ * @param buf_size the size of the @p buf buffer in characters,
+ * must not be zero
+ * @param[out] buf the buffer to fill
+ * @param number the number to convert
+ */
+static void
+mhdt_num_to_hex_w (size_t buf_size,
+ wchar_t *buf,
+ uint_least32_t number)
+{
+ static const wchar_t hex_map[] = L"0123456789abcdef";
+ size_t p = buf_size - 1u;
+ if (p > buf_size)
+ abort ();
+ buf[p] = L'\0';
+ while (--p < buf_size)
+ {
+ buf[p] = hex_map[number & 0xFu];
+ number >>= 4u;
+ }
+}
+
+
+# define mhdt_WSTR_(str) L ## str
+# define mhdt_WSTR(str) mhdt_WSTR_(str)
+
+# define MHDT_TMP_FILE_BASENAME_PREFIX_L\
+ mhdt_WSTR(MHDT_TMP_FILE_BASENAME_PREFIX)
+
+/**
+ * Create a temporary file that is deleted automatically when the FD is
+ * closed.
+ *
+ * The file is opened for both reading and writing in binary mode.
+ * The FD is not inherited by child processes.
+ *
+ * The locations for temporary files are tried in the following order:
+ * + the directories set by the "TMP" and the "TEMP" environment variables,
+ * + the current directory,
+ * + the directory set by "USERPROFILE",
+ * + and the system default directory for temporary files.
+ * The locations that are unset, too long or duplicated are skipped.
+ *
+ * @return the FD of the created file on success,
+ * -1 if the file cannot be created in any of the locations
+ */
+static int
+mhdt_create_tmpfile_w32 (void)
+{
+ static const wchar_t basename_prfx[] = MHDT_TMP_FILE_BASENAME_PREFIX_L;
+ const wchar_t *tmp_dir;
+ unsigned int i;
+
+ for (i = 0u; NULL != (tmp_dir = mhdt_get_tmpfile_dir_w32 (i)); ++i)
+ {
+ wchar_t tmp_filename[MHDT_FILENAME_MAX
+ + mhdt_ARR_CNT (basename_prfx)
+ + MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN];
+ int tmpfile_fd;
+ size_t tmp_dir_len;
+ static volatile unsigned int cnt = 0u;
+ int t;
+ int dir_access_checked;
+ uint_least32_t suff_num;
+
+ tmp_dir_len = wcslen (tmp_dir);
+ wmemcpy (tmp_filename,
+ tmp_dir,
+ tmp_dir_len);
+ wmemcpy (tmp_filename + tmp_dir_len,
+ basename_prfx,
+ mhdt_ARR_CNT (basename_prfx));
+
+ tmpfile_fd = -1;
+ dir_access_checked = 0;
+
+ suff_num = mhdtl_mix32 ((uint_least32_t)GetCurrentProcessId (),
+ (uint_least32_t)mhd_PTR_TO_INT (tmp_filename));
+ for (t = 0; mhdt_MAX_TRIES > t; ++t)
+ {
+ wchar_t *suff_ptr;
+ suff_num = mhdtl_mix32 (suff_num,
+ (uint_least32_t)cnt++);
+ suff_ptr = tmp_filename + tmp_dir_len + mhdt_ARR_CNT (basename_prfx) - 1u;
+
+ mhdt_num_to_hex_w (MHDT_TMP_FILE_UNIQUE_SUFFIX_LEN + 1u,
+ suff_ptr,
+ suff_num);
+
+ tmpfile_fd = _wopen (tmp_filename,
+ _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY
+ | _O_TEMPORARY | _O_SHORT_LIVED | _O_NOINHERIT,
+ _S_IREAD | _S_IWRITE);
+ if (0 <= tmpfile_fd)
+ break;
+ if (EEXIST == errno)
+ continue; /* Filename collision, try another filename */
+ if (EACCES == errno)
+ {
+ HANDLE h;
+ if (dir_access_checked)
+ continue;
+
+ h = CreateFileW (tmp_dir,
+ FILE_ADD_FILE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE
+ | FILE_SHARE_DELETE,
+ NULL,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS, /* Required to open a directory */
+ NULL);
+ if (INVALID_HANDLE_VALUE == h)
+ break; /* Directory not accessible, try next directory */
+ (void)CloseHandle (h);
+ dir_access_checked = !0;
+ continue; /* Conflict with a file being deleted, try another filename */
+ }
+ break; /* Avoid useless retries */
+ }
+
+ if (0 > tmpfile_fd)
+ continue; /* Try next directory */
+
+ return tmpfile_fd;
+ }
+
+ return -1;
+}
+
+
+#endif /* W32 */
+
+int
+MHDT_create_tmpfile (void)
+{
+#ifndef mhdt_USE_W32
+ return mhdt_create_tmpfile_posix ();
+#else
+ return mhdt_create_tmpfile_w32 ();
+#endif
+}
diff --git a/src/tests/client_server/mhdt_tmpfile.h b/src/tests/client_server/mhdt_tmpfile.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
+/*
+ This file is part of GNU libmicrohttpd.
+ Copyright (C) 2026 Evgeny Grin (Karlson2k)
+
+ GNU libmicrohttpd 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.
+
+ GNU libmicrohttpd 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.
+
+ Alternatively, you can redistribute GNU libmicrohttpd and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of
+ the License, or (at your option) any later version, together
+ with the eCos exception, as follows:
+
+ As a special exception, if other files instantiate templates or
+ use macros or inline functions from this file, or you compile this
+ file and link it with other works to produce a work based on this
+ file, this file does not by itself cause the resulting work to be
+ covered by the GNU General Public License. However the source code
+ for this file must still be made available in accordance with
+ section (3) of the GNU General Public License v2.
+
+ This exception does not invalidate any other reasons why a work
+ based on this file might be covered by the GNU General Public
+ License.
+
+ You should have received copies of the GNU Lesser General Public
+ License and the GNU General Public License along with this library;
+ if not, see <https://www.gnu.org/licenses/>.
+*/
+
+/**
+ * @file src/tests/client_server/mhdt_tmpfile.h
+ * @brief MHD test framework temporary file helpers
+ * @author Karlson2k (Evgeny Grin)
+ */
+#ifndef MHDT_TMPFILE_H
+#define MHDT_TMPFILE_H 1
+
+#include "mhd_sys_options.h"
+
+/**
+ * Create a temporary file that is deleted automatically when the FD is
+ * closed.
+ *
+ * The locations for temporary files are tried in the following order:
+ * + the directory set by the "TMPDIR" environment variable (POSIX only),
+ * + the directories set by the "TMP" and the "TEMP" environment variables,
+ * + the current directory,
+ * + the directory specified by the P_tmpdir macro, if defined (POSIX only),
+ * + the "/tmp" directory (POSIX only),
+ * + the directory set by the "USERPROFILE" environment variable (W32 only),
+ * + and the system default directory for temporary files (W32 only).
+ * The locations that are unset, too long, duplicated or not usable are
+ * skipped.
+ *
+ * @return the FD of the created file on success,
+ * -1 if the file cannot be created in any of the locations
+ */
+int
+MHDT_create_tmpfile (void);
+
+#endif /* ! MHDT_TMPFILE_H */