diff --git a/tests/data/test3402 b/tests/data/test3402
new file mode 100644
index 0000000000..23fcb62f4d
--- /dev/null
+++ b/tests/data/test3402
@@ -0,0 +1,19 @@
+
+
+
+
+unittest
+slist
+
+
+
+# Client-side
+
+
+unittest
+
+
+slist unit tests (curl_slist_append, Curl_slist_duplicate, curl_slist_free_all)
+
+
+
diff --git a/tests/data/test3403 b/tests/data/test3403
new file mode 100644
index 0000000000..c1b6e71dc8
--- /dev/null
+++ b/tests/data/test3403
@@ -0,0 +1,19 @@
+
+
+
+
+unittest
+escape
+
+
+
+# Client-side
+
+
+unittest
+
+
+escape unit tests (curl_easy_escape, curl_easy_unescape, Curl_urldecode, Curl_hexencode, Curl_hexbyte)
+
+
+
diff --git a/tests/data/test3404 b/tests/data/test3404
new file mode 100644
index 0000000000..3ab72143c6
--- /dev/null
+++ b/tests/data/test3404
@@ -0,0 +1,19 @@
+
+
+
+
+unittest
+endian
+
+
+
+# Client-side
+
+
+unittest
+
+
+curl_endian unit tests (Curl_read16_le, Curl_read32_le, Curl_read16_be)
+
+
+
diff --git a/tests/data/test3405 b/tests/data/test3405
new file mode 100644
index 0000000000..a55e2fdce3
--- /dev/null
+++ b/tests/data/test3405
@@ -0,0 +1,19 @@
+
+
+
+
+unittest
+parsedate
+
+
+
+# Client-side
+
+
+unittest
+
+
+parsedate unit tests (curl_getdate: RFC 1123, RFC 850, asctime, compact, timezones, error cases)
+
+
+
diff --git a/tests/data/test3406 b/tests/data/test3406
new file mode 100644
index 0000000000..310aaecf36
--- /dev/null
+++ b/tests/data/test3406
@@ -0,0 +1,19 @@
+
+
+
+
+unittest
+strerror
+
+
+
+# Client-side
+
+
+unittest
+
+
+strerror unit tests (curl_easy_strerror, curl_multi_strerror, curl_share_strerror, curl_url_strerror)
+
+
+
diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc
index fd05c070e4..fe042ca05e 100644
--- a/tests/unit/Makefile.inc
+++ b/tests/unit/Makefile.inc
@@ -51,4 +51,5 @@ TESTS_C = \
unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c unit3219.c \
unit3227.c \
unit3300.c unit3301.c unit3302.c unit3303.c unit3304.c unit3306.c \
- unit3400.c
+ unit3400.c \
+ unit3402.c unit3403.c unit3404.c unit3405.c unit3406.c
diff --git a/tests/unit/unit3402.c b/tests/unit/unit3402.c
new file mode 100644
index 0000000000..fff015c6d8
--- /dev/null
+++ b/tests/unit/unit3402.c
@@ -0,0 +1,127 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, , et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+#include "slist.h"
+
+static CURLcode test_unit3402(const char *arg)
+{
+ UNITTEST_BEGIN_SIMPLE
+
+ struct curl_slist *list = NULL;
+ struct curl_slist *clone = NULL;
+ struct curl_slist *item = NULL;
+
+ /* --- curl_slist_append: append to NULL creates a new list --- */
+ list = curl_slist_append(NULL, "first");
+ abort_unless(list, "curl_slist_append on NULL should allocate a new list");
+ fail_unless(strcmp(list->data, "first") == 0,
+ "first node data should be 'first'");
+ fail_unless(!list->next, "single-element list must have next == NULL");
+
+ /* --- curl_slist_append: append a second element --- */
+ list = curl_slist_append(list, "second");
+ abort_unless(list, "curl_slist_append should return the list head");
+ fail_unless(strcmp(list->data, "first") == 0,
+ "head data must still be 'first' after second append");
+ abort_unless(list->next, "list must have a second node");
+ fail_unless(strcmp(list->next->data, "second") == 0,
+ "second node data should be 'second'");
+ fail_unless(!list->next->next, "two-element list tail must have next == NULL");
+
+ /* --- curl_slist_append: append a third element --- */
+ list = curl_slist_append(list, "third");
+ abort_unless(list && list->next && list->next->next,
+ "list must have three nodes after three appends");
+ fail_unless(strcmp(list->next->next->data, "third") == 0,
+ "third node data should be 'third'");
+ fail_unless(!list->next->next->next,
+ "three-element list tail must have next == NULL");
+
+ /* --- curl_slist_append: data is copied (mutation of original is safe) --- */
+ {
+ char buf[16];
+ struct curl_slist *copy_test;
+ memcpy(buf, "mutable", 8);
+ copy_test = curl_slist_append(NULL, buf);
+ abort_unless(copy_test, "append of mutable buffer should succeed");
+ memcpy(buf, "XXXXXXX", 7); /* mutate original */
+ fail_unless(strcmp(copy_test->data, "mutable") == 0,
+ "slist must own a copy, not a reference to the original");
+ curl_slist_free_all(copy_test);
+ }
+
+ /* --- Curl_slist_append_nodup: takes ownership of a malloc'd string --- */
+ {
+ char *owned = strdup("owned");
+ struct curl_slist *nodup_list;
+ abort_unless(owned, "strdup must succeed");
+ nodup_list = Curl_slist_append_nodup(NULL, owned);
+ abort_unless(nodup_list, "Curl_slist_append_nodup should succeed");
+ fail_unless(nodup_list->data == owned,
+ "nodup variant must store the exact pointer, not a copy");
+ curl_slist_free_all(nodup_list);
+ }
+
+ /* --- Curl_slist_duplicate: NULL input returns NULL --- */
+ clone = Curl_slist_duplicate(NULL);
+ fail_unless(!clone, "duplicating NULL list should return NULL");
+
+ /* --- Curl_slist_duplicate: clones all nodes with independent storage --- */
+ clone = Curl_slist_duplicate(list);
+ abort_unless(clone, "Curl_slist_duplicate should return a valid list");
+
+ /* verify each node matches by value */
+ item = clone;
+ fail_unless(strcmp(item->data, "first") == 0,
+ "cloned node 1 should be 'first'");
+ abort_unless(item->next, "clone must have at least 2 nodes");
+ item = item->next;
+ fail_unless(strcmp(item->data, "second") == 0,
+ "cloned node 2 should be 'second'");
+ abort_unless(item->next, "clone must have 3 nodes");
+ item = item->next;
+ fail_unless(strcmp(item->data, "third") == 0,
+ "cloned node 3 should be 'third'");
+ fail_unless(!item->next, "cloned list must end after 3 nodes");
+
+ /* verify the clone is independent: mutating original does not affect clone */
+ fail_unless(clone->data != list->data,
+ "clone nodes must use separate storage from the original");
+
+ /* --- curl_slist_free_all: freeing NULL is a no-op --- */
+ curl_slist_free_all(NULL); /* must not crash */
+
+ /* --- curl_slist_free_all: frees a single-element list --- */
+ {
+ struct curl_slist *single = curl_slist_append(NULL, "only");
+ abort_unless(single, "single-element list must be created");
+ curl_slist_free_all(single); /* must not crash or leak */
+ }
+
+ /* cleanup */
+ curl_slist_free_all(clone);
+ curl_slist_free_all(list);
+
+ UNITTEST_END_SIMPLE
+}
diff --git a/tests/unit/unit3403.c b/tests/unit/unit3403.c
new file mode 100644
index 0000000000..0812746ab1
--- /dev/null
+++ b/tests/unit/unit3403.c
@@ -0,0 +1,220 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, , et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+#include "escape.h"
+
+static CURLcode test_unit3403(const char *arg)
+{
+ UNITTEST_BEGIN_SIMPLE
+
+ char *result = NULL;
+ size_t olen = 0;
+ CURLcode rc;
+
+ /* ==================================================================
+ * curl_easy_escape
+ * ================================================================== */
+
+ /* unreserved characters must pass through unencoded */
+ result = curl_easy_escape(NULL, "azAZ09-._~", 0);
+ abort_unless(result, "escape of unreserved chars must succeed");
+ fail_unless(strcmp(result, "azAZ09-._~") == 0,
+ "unreserved chars must not be percent-encoded");
+ curl_free(result);
+
+ /* space must become %20 */
+ result = curl_easy_escape(NULL, " ", 0);
+ abort_unless(result, "escape of space must succeed");
+ fail_unless(strcmp(result, "%20") == 0, "space must encode to %20");
+ curl_free(result);
+
+ /* all common special chars get encoded */
+ result = curl_easy_escape(NULL, "hello world!", 0);
+ abort_unless(result, "escape of 'hello world!' must succeed");
+ fail_unless(strcmp(result, "hello%20world%21") == 0,
+ "space->%20 and !->%21");
+ curl_free(result);
+
+ /* high-byte value 0xFF */
+ result = curl_easy_escape(NULL, "\xff", 1);
+ abort_unless(result, "escape of 0xFF must succeed");
+ fail_unless(strcmp(result, "%FF") == 0, "0xFF must encode to %FF");
+ curl_free(result);
+
+ /* empty string returns empty string, not NULL */
+ result = curl_easy_escape(NULL, "", 0);
+ abort_unless(result, "escape of empty string must succeed");
+ fail_unless(strcmp(result, "") == 0, "escape of empty string must be empty");
+ curl_free(result);
+
+ /* explicit length overrides strlen — encode only 5 chars of a longer string */
+ result = curl_easy_escape(NULL, "hello world", 5);
+ abort_unless(result, "escape with explicit length must succeed");
+ fail_unless(strcmp(result, "hello") == 0,
+ "explicit length=5 must produce only 'hello'");
+ curl_free(result);
+
+ /* NULL string must return NULL */
+ result = curl_easy_escape(NULL, NULL, 0);
+ fail_unless(!result, "escape of NULL string must return NULL");
+
+ /* negative length must return NULL */
+ result = curl_easy_escape(NULL, "hello", -1);
+ fail_unless(!result, "escape with negative length must return NULL");
+
+ /* ==================================================================
+ * curl_easy_unescape
+ * ================================================================== */
+
+ /* basic percent-decode */
+ result = curl_easy_unescape(NULL, "%68%65%6c%6c%6f", 0, NULL);
+ abort_unless(result, "unescape of %%68%%65%%6c%%6c%%6f must succeed");
+ fail_unless(strcmp(result, "hello") == 0,
+ "%%68%%65%%6c%%6c%%6f must decode to 'hello'");
+ curl_free(result);
+
+ /* mixed encoded and plain text */
+ result = curl_easy_unescape(NULL, "hello%20world", 0, NULL);
+ abort_unless(result, "unescape of 'hello%%20world' must succeed");
+ fail_unless(strcmp(result, "hello world") == 0,
+ "hello%%20world must decode to 'hello world'");
+ curl_free(result);
+
+ /* output length reported correctly */
+ {
+ int outlen = -1;
+ result = curl_easy_unescape(NULL, "%41%42%43", 0, &outlen);
+ abort_unless(result, "unescape of %%41%%42%%43 must succeed");
+ fail_unless(strcmp(result, "ABC") == 0,
+ "%%41%%42%%43 must decode to 'ABC'");
+ fail_unless(outlen == 3, "output length must be 3");
+ curl_free(result);
+ }
+
+ /* uppercase hex digits also work */
+ result = curl_easy_unescape(NULL, "%2F", 0, NULL);
+ abort_unless(result, "unescape of %%2F must succeed");
+ fail_unless(strcmp(result, "/") == 0, "%%2F must decode to '/'");
+ curl_free(result);
+
+ /* a lone % that is not followed by two hex digits passes through */
+ result = curl_easy_unescape(NULL, "100%", 0, NULL);
+ abort_unless(result, "unescape of '100%%' must succeed");
+ fail_unless(strcmp(result, "100%") == 0,
+ "trailing lone %% must be kept as-is");
+ curl_free(result);
+
+ /* empty input */
+ result = curl_easy_unescape(NULL, "", 0, NULL);
+ abort_unless(result, "unescape of empty string must succeed");
+ fail_unless(strcmp(result, "") == 0, "unescape of empty must be empty");
+ curl_free(result);
+
+ /* negative length must return NULL */
+ result = curl_easy_unescape(NULL, "hello", -1, NULL);
+ fail_unless(!result, "unescape with negative length must return NULL");
+
+ /* ==================================================================
+ * Curl_urldecode — internal decoder with ctrl-char rejection
+ * ================================================================== */
+
+ /* REJECT_NADA: accepts everything including control chars */
+ rc = Curl_urldecode("%01%02", 0, &result, &olen, REJECT_NADA);
+ fail_unless(rc == CURLE_OK, "REJECT_NADA must accept control chars");
+ fail_unless(olen == 2, "decoded length must be 2");
+ curl_free(result);
+
+ /* REJECT_CTRL: rejects decoded bytes < 0x20 */
+ rc = Curl_urldecode("%01", 0, &result, &olen, REJECT_CTRL);
+ fail_unless(rc == CURLE_URL_MALFORMAT,
+ "REJECT_CTRL must reject control byte 0x01");
+
+ /* REJECT_CTRL: allows regular bytes */
+ rc = Curl_urldecode("hello%20world", 0, &result, &olen, REJECT_CTRL);
+ fail_unless(rc == CURLE_OK,
+ "REJECT_CTRL must allow non-control encoded chars");
+ fail_unless(olen == 11, "decoded length must be 11");
+ fail_unless(strcmp(result, "hello world") == 0,
+ "REJECT_CTRL decode of 'hello%%20world' must be 'hello world'");
+ curl_free(result);
+
+ /* REJECT_ZERO: rejects a decoded NUL byte */
+ rc = Curl_urldecode("%00", 0, &result, &olen, REJECT_ZERO);
+ fail_unless(rc == CURLE_URL_MALFORMAT,
+ "REJECT_ZERO must reject encoded NUL byte");
+
+ /* REJECT_ZERO: allows non-NUL bytes */
+ rc = Curl_urldecode("ok", 0, &result, &olen, REJECT_ZERO);
+ fail_unless(rc == CURLE_OK, "REJECT_ZERO must allow normal bytes");
+ fail_unless(olen == 2, "decoded length of 'ok' must be 2");
+ curl_free(result);
+
+ /* ==================================================================
+ * Curl_hexencode
+ * ================================================================== */
+
+ {
+ const unsigned char src[] = {0x00, 0x0f, 0x10, 0xff, 0xab};
+ unsigned char out[11]; /* 5 bytes * 2 hex chars + NUL */
+ memset(out, 0xcc, sizeof(out));
+ Curl_hexencode(src, sizeof(src), out, sizeof(out));
+ fail_unless(strcmp((char *)out, "000f10ffab") == 0,
+ "Curl_hexencode must produce lowercase hex");
+ fail_unless(out[10] == '\0', "Curl_hexencode must NUL-terminate");
+ }
+
+ /* single byte */
+ {
+ const unsigned char src[] = {0xbe};
+ unsigned char out[3];
+ Curl_hexencode(src, 1, out, sizeof(out));
+ fail_unless(strcmp((char *)out, "be") == 0,
+ "Curl_hexencode single byte 0xBE must be 'be'");
+ }
+
+ /* ==================================================================
+ * Curl_hexbyte
+ * ================================================================== */
+
+ {
+ unsigned char dest[2];
+ Curl_hexbyte(dest, 0x00);
+ fail_unless(dest[0] == '0' && dest[1] == '0',
+ "Curl_hexbyte(0x00) must produce '00'");
+
+ Curl_hexbyte(dest, 0xFF);
+ fail_unless(dest[0] == 'F' && dest[1] == 'F',
+ "Curl_hexbyte(0xFF) must produce 'FF' (uppercase)");
+
+ Curl_hexbyte(dest, 0x1A);
+ fail_unless(dest[0] == '1' && dest[1] == 'A',
+ "Curl_hexbyte(0x1A) must produce '1A'");
+
+ Curl_hexbyte(dest, 0xa3);
+ fail_unless(dest[0] == 'A' && dest[1] == '3',
+ "Curl_hexbyte(0xa3) must produce 'A3'");
+ }
+
+ UNITTEST_END_SIMPLE
+}
diff --git a/tests/unit/unit3404.c b/tests/unit/unit3404.c
new file mode 100644
index 0000000000..752f5ab0e0
--- /dev/null
+++ b/tests/unit/unit3404.c
@@ -0,0 +1,171 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, , et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+#include "curl_endian.h"
+
+static CURLcode test_unit3404(const char *arg)
+{
+ UNITTEST_BEGIN_SIMPLE
+
+ /* ==================================================================
+ * Curl_read16_le — little-endian 16-bit reader
+ * ================================================================== */
+
+ /* 0x0102 stored little-endian: low byte first */
+ {
+ const unsigned char buf[] = {0x02, 0x01};
+ fail_unless(Curl_read16_le(buf) == 0x0102,
+ "Curl_read16_le({0x02,0x01}) must return 0x0102");
+ }
+
+ /* zero */
+ {
+ const unsigned char buf[] = {0x00, 0x00};
+ fail_unless(Curl_read16_le(buf) == 0x0000,
+ "Curl_read16_le all-zero must return 0");
+ }
+
+ /* max value 0xFFFF */
+ {
+ const unsigned char buf[] = {0xFF, 0xFF};
+ fail_unless(Curl_read16_le(buf) == 0xFFFF,
+ "Curl_read16_le({0xFF,0xFF}) must return 0xFFFF");
+ }
+
+ /* byte order check: low nibble in first byte, high nibble in second */
+ {
+ const unsigned char buf[] = {0xCD, 0xAB};
+ fail_unless(Curl_read16_le(buf) == 0xABCD,
+ "Curl_read16_le({0xCD,0xAB}) must return 0xABCD");
+ }
+
+ /* 0x0100 — only high byte set */
+ {
+ const unsigned char buf[] = {0x00, 0x01};
+ fail_unless(Curl_read16_le(buf) == 0x0100,
+ "Curl_read16_le({0x00,0x01}) must return 0x0100");
+ }
+
+ /* ==================================================================
+ * Curl_read32_le — little-endian 32-bit reader
+ * ================================================================== */
+
+ /* 0x01020304 little-endian: lowest byte first */
+ {
+ const unsigned char buf[] = {0x04, 0x03, 0x02, 0x01};
+ fail_unless(Curl_read32_le(buf) == 0x01020304U,
+ "Curl_read32_le({0x04,0x03,0x02,0x01}) must return 0x01020304");
+ }
+
+ /* zero */
+ {
+ const unsigned char buf[] = {0x00, 0x00, 0x00, 0x00};
+ fail_unless(Curl_read32_le(buf) == 0x00000000U,
+ "Curl_read32_le all-zero must return 0");
+ }
+
+ /* max value 0xFFFFFFFF */
+ {
+ const unsigned char buf[] = {0xFF, 0xFF, 0xFF, 0xFF};
+ fail_unless(Curl_read32_le(buf) == 0xFFFFFFFFU,
+ "Curl_read32_le all-0xFF must return 0xFFFFFFFF");
+ }
+
+ /* only highest byte set: 0x01000000 */
+ {
+ const unsigned char buf[] = {0x00, 0x00, 0x00, 0x01};
+ fail_unless(Curl_read32_le(buf) == 0x01000000U,
+ "Curl_read32_le({0,0,0,1}) must return 0x01000000");
+ }
+
+ /* only lowest byte set: 0x00000001 */
+ {
+ const unsigned char buf[] = {0x01, 0x00, 0x00, 0x00};
+ fail_unless(Curl_read32_le(buf) == 0x00000001U,
+ "Curl_read32_le({1,0,0,0}) must return 0x00000001");
+ }
+
+ /* known value 0xDEADBEEF little-endian */
+ {
+ const unsigned char buf[] = {0xEF, 0xBE, 0xAD, 0xDE};
+ fail_unless(Curl_read32_le(buf) == 0xDEADBEEFU,
+ "Curl_read32_le(DEADBEEF LE) must return 0xDEADBEEF");
+ }
+
+ /* ==================================================================
+ * Curl_read16_be — big-endian 16-bit reader
+ * ================================================================== */
+
+ /* 0x0102 stored big-endian: high byte first */
+ {
+ const unsigned char buf[] = {0x01, 0x02};
+ fail_unless(Curl_read16_be(buf) == 0x0102,
+ "Curl_read16_be({0x01,0x02}) must return 0x0102");
+ }
+
+ /* zero */
+ {
+ const unsigned char buf[] = {0x00, 0x00};
+ fail_unless(Curl_read16_be(buf) == 0x0000,
+ "Curl_read16_be all-zero must return 0");
+ }
+
+ /* max value 0xFFFF */
+ {
+ const unsigned char buf[] = {0xFF, 0xFF};
+ fail_unless(Curl_read16_be(buf) == 0xFFFF,
+ "Curl_read16_be({0xFF,0xFF}) must return 0xFFFF");
+ }
+
+ /* byte order check: high nibble in first byte */
+ {
+ const unsigned char buf[] = {0xAB, 0xCD};
+ fail_unless(Curl_read16_be(buf) == 0xABCD,
+ "Curl_read16_be({0xAB,0xCD}) must return 0xABCD");
+ }
+
+ /* 0x0100 big-endian */
+ {
+ const unsigned char buf[] = {0x01, 0x00};
+ fail_unless(Curl_read16_be(buf) == 0x0100,
+ "Curl_read16_be({0x01,0x00}) must return 0x0100");
+ }
+
+ /* ==================================================================
+ * Cross-check: LE and BE must differ for asymmetric inputs
+ * ================================================================== */
+ {
+ const unsigned char buf[] = {0x12, 0x34};
+ unsigned short le = Curl_read16_le(buf);
+ unsigned short be = Curl_read16_be(buf);
+ fail_unless(le == 0x3412,
+ "Curl_read16_le({0x12,0x34}) must return 0x3412");
+ fail_unless(be == 0x1234,
+ "Curl_read16_be({0x12,0x34}) must return 0x1234");
+ fail_unless(le != be,
+ "LE and BE of an asymmetric buffer must differ");
+ }
+
+ UNITTEST_END_SIMPLE
+}
diff --git a/tests/unit/unit3405.c b/tests/unit/unit3405.c
new file mode 100644
index 0000000000..70049fb421
--- /dev/null
+++ b/tests/unit/unit3405.c
@@ -0,0 +1,159 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, , et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+
+/* curl_getdate is a public API declared in curl/curl.h, already pulled in
+ via unitcheck.h -> curl_setup.h -> curl.h */
+
+static CURLcode test_unit3405(const char *arg)
+{
+ UNITTEST_BEGIN_SIMPLE
+
+#ifndef CURL_DISABLE_PARSEDATE
+
+ time_t t;
+
+ /* ==================================================================
+ * RFC 822 / RFC 1123 format: "Sun, 06 Nov 1994 08:49:37 GMT"
+ * Expected epoch: 784111777
+ * ================================================================== */
+ t = curl_getdate("Sun, 06 Nov 1994 08:49:37 GMT", NULL);
+ fail_unless(t == (time_t)784111777,
+ "RFC 1123 date must parse to epoch 784111777");
+
+ /* ==================================================================
+ * RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT"
+ * ================================================================== */
+ t = curl_getdate("Sunday, 06-Nov-94 08:49:37 GMT", NULL);
+ fail_unless(t == (time_t)784111777,
+ "RFC 850 date must parse to epoch 784111777");
+
+ /* ==================================================================
+ * ANSI C asctime() format: "Sun Nov 6 08:49:37 1994"
+ * ================================================================== */
+ t = curl_getdate("Sun Nov 6 08:49:37 1994", NULL);
+ fail_unless(t == (time_t)784111777,
+ "asctime() format must parse to epoch 784111777");
+
+ /* ==================================================================
+ * Compact numerical: "19941106 08:49:37 GMT"
+ * ================================================================== */
+ t = curl_getdate("19941106 08:49:37 GMT", NULL);
+ fail_unless(t == (time_t)784111777,
+ "compact YYYYMMDD format must parse to epoch 784111777");
+
+ /* ==================================================================
+ * Date without time (time defaults to 00:00:00)
+ * "06 Nov 1994" -> 784080000
+ * ================================================================== */
+ t = curl_getdate("06 Nov 1994", NULL);
+ fail_unless(t == (time_t)784080000,
+ "date without time must default to 00:00:00 UTC");
+
+ /* ==================================================================
+ * Epoch itself: "01 Jan 1970 00:00:00 GMT" -> 0
+ * ================================================================== */
+ t = curl_getdate("01 Jan 1970 00:00:00 GMT", NULL);
+ fail_unless(t == (time_t)0,
+ "Unix epoch date must parse to 0");
+
+ /* ==================================================================
+ * RFC 822 with numeric timezone offset "+0200"
+ * "12 Sep 2004 15:05:58 +0200" -> UTC 13:05:58 on 2004-09-12
+ * Expected epoch: 1094994358
+ * ================================================================== */
+ t = curl_getdate("12 Sep 2004 15:05:58 +0200", NULL);
+ fail_unless(t == (time_t)1094994358,
+ "date with +0200 offset must be adjusted to UTC");
+
+ /* ==================================================================
+ * RFC 822 with negative timezone offset "-0700"
+ * "12 Sep 2004 15:05:58 -0700" -> UTC 22:05:58 on 2004-09-12
+ * Expected epoch: 1095026758
+ * ================================================================== */
+ t = curl_getdate("12 Sep 2004 15:05:58 -0700", NULL);
+ fail_unless(t == (time_t)1095026758,
+ "date with -0700 offset must be adjusted to UTC");
+
+ /* ==================================================================
+ * Named timezone: CET = UTC+1 (offset -60 min in curl's tz table)
+ * "06 Nov 1994 08:49:37 CET" -> subtract 3600s from GMT equivalent
+ * Expected epoch: 784111777 - 3600 = 784108177
+ * ================================================================== */
+ t = curl_getdate("06 Nov 1994 08:49:37 CET", NULL);
+ fail_unless(t == (time_t)784108177,
+ "CET timezone must subtract 3600s from the UTC value");
+
+ /* ==================================================================
+ * Two-digit year >= 70 -> 19xx
+ * "06-Nov-94 08:49:37 GMT" -> year 1994
+ * ================================================================== */
+ t = curl_getdate("06-Nov-94 08:49:37 GMT", NULL);
+ fail_unless(t == (time_t)784111777,
+ "two-digit year 94 must be treated as 1994");
+
+ /* ==================================================================
+ * Two-digit year < 70 -> 20xx
+ * "06 Nov 04 08:49:37 GMT" -> year 2004
+ * Expected epoch: 1099730977
+ * ================================================================== */
+ t = curl_getdate("06 Nov 04 08:49:37 GMT", NULL);
+ fail_unless(t == (time_t)1099730977,
+ "two-digit year 04 must be treated as 2004");
+
+ /* ==================================================================
+ * Earliest valid year boundary: year 1583 (Gregorian calendar start).
+ * Anything before 1583 must fail.
+ * ================================================================== */
+ t = curl_getdate("01 Jan 1582 00:00:00 GMT", NULL);
+ fail_unless(t == (time_t)-1,
+ "year 1582 is before Gregorian calendar, must return -1");
+
+ /* ==================================================================
+ * Invalid / garbage input must return -1
+ * ================================================================== */
+ t = curl_getdate("not a date at all", NULL);
+ fail_unless(t == (time_t)-1,
+ "garbage input must return -1");
+
+ t = curl_getdate("", NULL);
+ fail_unless(t == (time_t)-1,
+ "empty string must return -1");
+
+ t = curl_getdate("32 Nov 1994 08:49:37 GMT", NULL);
+ fail_unless(t == (time_t)-1,
+ "day=32 is illegal, must return -1");
+
+ t = curl_getdate("06 Nov 1994 25:00:00 GMT", NULL);
+ fail_unless(t == (time_t)-1,
+ "hour=25 is illegal, must return -1");
+
+ t = curl_getdate("06 Foo 1994 08:49:37 GMT", NULL);
+ fail_unless(t == (time_t)-1,
+ "unknown month name must return -1");
+
+#endif /* CURL_DISABLE_PARSEDATE */
+
+ UNITTEST_END_SIMPLE
+}
diff --git a/tests/unit/unit3406.c b/tests/unit/unit3406.c
new file mode 100644
index 0000000000..c130e10b44
--- /dev/null
+++ b/tests/unit/unit3406.c
@@ -0,0 +1,222 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, , et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+
+/* All four strerror functions are public API, declared in curl/curl.h which
+ is pulled in transitively through unitcheck.h -> curl_setup.h */
+
+static CURLcode test_unit3406(const char *arg)
+{
+ UNITTEST_BEGIN_SIMPLE
+
+ const char *s;
+
+ /* ==================================================================
+ * curl_easy_strerror
+ * In CURLVERBOSE builds each code maps to a unique, non-empty string.
+ * In non-verbose builds CURLE_OK -> "No error", anything else -> "Error".
+ * Both modes must return a non-NULL, non-empty string for every value.
+ * ================================================================== */
+
+ /* CURLE_OK always maps to a non-empty "success" string */
+ s = curl_easy_strerror(CURLE_OK);
+ abort_unless(s, "curl_easy_strerror(CURLE_OK) must not return NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_easy_strerror(CURLE_OK) must not be empty");
+
+ /* A handful of well-known error codes must return non-NULL/non-empty */
+ s = curl_easy_strerror(CURLE_UNSUPPORTED_PROTOCOL);
+ abort_unless(s, "curl_easy_strerror(CURLE_UNSUPPORTED_PROTOCOL) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_easy_strerror(CURLE_UNSUPPORTED_PROTOCOL) not empty");
+
+ s = curl_easy_strerror(CURLE_COULDNT_CONNECT);
+ abort_unless(s, "curl_easy_strerror(CURLE_COULDNT_CONNECT) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_easy_strerror(CURLE_COULDNT_CONNECT) not empty");
+
+ s = curl_easy_strerror(CURLE_OUT_OF_MEMORY);
+ abort_unless(s, "curl_easy_strerror(CURLE_OUT_OF_MEMORY) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_easy_strerror(CURLE_OUT_OF_MEMORY) not empty");
+
+ s = curl_easy_strerror(CURLE_OPERATION_TIMEDOUT);
+ abort_unless(s, "curl_easy_strerror(CURLE_OPERATION_TIMEDOUT) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_easy_strerror(CURLE_OPERATION_TIMEDOUT) not empty");
+
+ s = curl_easy_strerror(CURLE_SSL_CONNECT_ERROR);
+ abort_unless(s, "curl_easy_strerror(CURLE_SSL_CONNECT_ERROR) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_easy_strerror(CURLE_SSL_CONNECT_ERROR) not empty");
+
+ /* An out-of-range code must still return a non-NULL string, not crash */
+ s = curl_easy_strerror((CURLcode)9999);
+ abort_unless(s, "curl_easy_strerror(9999) must not return NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_easy_strerror(9999) must return a non-empty fallback");
+
+ /* In CURLVERBOSE builds different codes must produce different strings */
+#ifdef CURLVERBOSE
+ {
+ const char *s_ok = curl_easy_strerror(CURLE_OK);
+ const char *s_oom = curl_easy_strerror(CURLE_OUT_OF_MEMORY);
+ fail_unless(strcmp(s_ok, s_oom) != 0,
+ "distinct error codes must map to distinct strings");
+ }
+#endif
+
+ /* ==================================================================
+ * curl_multi_strerror
+ * ================================================================== */
+
+ s = curl_multi_strerror(CURLM_OK);
+ abort_unless(s, "curl_multi_strerror(CURLM_OK) must not return NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_multi_strerror(CURLM_OK) must not be empty");
+
+ s = curl_multi_strerror(CURLM_BAD_HANDLE);
+ abort_unless(s, "curl_multi_strerror(CURLM_BAD_HANDLE) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_multi_strerror(CURLM_BAD_HANDLE) not empty");
+
+ s = curl_multi_strerror(CURLM_OUT_OF_MEMORY);
+ abort_unless(s, "curl_multi_strerror(CURLM_OUT_OF_MEMORY) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_multi_strerror(CURLM_OUT_OF_MEMORY) not empty");
+
+ s = curl_multi_strerror(CURLM_INTERNAL_ERROR);
+ abort_unless(s, "curl_multi_strerror(CURLM_INTERNAL_ERROR) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_multi_strerror(CURLM_INTERNAL_ERROR) not empty");
+
+ /* Out-of-range must not crash */
+ s = curl_multi_strerror((CURLMcode)9999);
+ abort_unless(s, "curl_multi_strerror(9999) must not return NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_multi_strerror(9999) must return a non-empty fallback");
+
+#ifdef CURLVERBOSE
+ {
+ const char *s_ok = curl_multi_strerror(CURLM_OK);
+ const char *s_bad = curl_multi_strerror(CURLM_BAD_HANDLE);
+ fail_unless(strcmp(s_ok, s_bad) != 0,
+ "CURLM_OK and CURLM_BAD_HANDLE must have distinct strings");
+ }
+#endif
+
+ /* ==================================================================
+ * curl_share_strerror
+ * ================================================================== */
+
+ s = curl_share_strerror(CURLSHE_OK);
+ abort_unless(s, "curl_share_strerror(CURLSHE_OK) must not return NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_share_strerror(CURLSHE_OK) must not be empty");
+
+ s = curl_share_strerror(CURLSHE_BAD_OPTION);
+ abort_unless(s, "curl_share_strerror(CURLSHE_BAD_OPTION) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_share_strerror(CURLSHE_BAD_OPTION) not empty");
+
+ s = curl_share_strerror(CURLSHE_IN_USE);
+ abort_unless(s, "curl_share_strerror(CURLSHE_IN_USE) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_share_strerror(CURLSHE_IN_USE) not empty");
+
+ s = curl_share_strerror(CURLSHE_INVALID);
+ abort_unless(s, "curl_share_strerror(CURLSHE_INVALID) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_share_strerror(CURLSHE_INVALID) not empty");
+
+ s = curl_share_strerror(CURLSHE_NOMEM);
+ abort_unless(s, "curl_share_strerror(CURLSHE_NOMEM) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_share_strerror(CURLSHE_NOMEM) not empty");
+
+ s = curl_share_strerror(CURLSHE_NOT_BUILT_IN);
+ abort_unless(s, "curl_share_strerror(CURLSHE_NOT_BUILT_IN) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_share_strerror(CURLSHE_NOT_BUILT_IN) not empty");
+
+#ifdef CURLVERBOSE
+ {
+ const char *s_ok = curl_share_strerror(CURLSHE_OK);
+ const char *s_err = curl_share_strerror(CURLSHE_IN_USE);
+ fail_unless(strcmp(s_ok, s_err) != 0,
+ "CURLSHE_OK and CURLSHE_IN_USE must have distinct strings");
+ }
+#endif
+
+ /* ==================================================================
+ * curl_url_strerror
+ * ================================================================== */
+
+ s = curl_url_strerror(CURLUE_OK);
+ abort_unless(s, "curl_url_strerror(CURLUE_OK) must not return NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_url_strerror(CURLUE_OK) must not be empty");
+
+ s = curl_url_strerror(CURLUE_BAD_HANDLE);
+ abort_unless(s, "curl_url_strerror(CURLUE_BAD_HANDLE) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_url_strerror(CURLUE_BAD_HANDLE) not empty");
+
+ s = curl_url_strerror(CURLUE_MALFORMED_INPUT);
+ abort_unless(s, "curl_url_strerror(CURLUE_MALFORMED_INPUT) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_url_strerror(CURLUE_MALFORMED_INPUT) not empty");
+
+ s = curl_url_strerror(CURLUE_NO_SCHEME);
+ abort_unless(s, "curl_url_strerror(CURLUE_NO_SCHEME) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_url_strerror(CURLUE_NO_SCHEME) not empty");
+
+ s = curl_url_strerror(CURLUE_NO_HOST);
+ abort_unless(s, "curl_url_strerror(CURLUE_NO_HOST) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_url_strerror(CURLUE_NO_HOST) not empty");
+
+ s = curl_url_strerror(CURLUE_BAD_IPV6);
+ abort_unless(s, "curl_url_strerror(CURLUE_BAD_IPV6) != NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_url_strerror(CURLUE_BAD_IPV6) not empty");
+
+ /* Out-of-range must not crash */
+ s = curl_url_strerror((CURLUcode)9999);
+ abort_unless(s, "curl_url_strerror(9999) must not return NULL");
+ fail_unless(strlen(s) > 0,
+ "curl_url_strerror(9999) must return a non-empty fallback");
+
+#ifdef CURLVERBOSE
+ {
+ const char *s_ok = curl_url_strerror(CURLUE_OK);
+ const char *s_err = curl_url_strerror(CURLUE_NO_HOST);
+ fail_unless(strcmp(s_ok, s_err) != 0,
+ "CURLUE_OK and CURLUE_NO_HOST must have distinct strings");
+ }
+#endif
+
+ UNITTEST_END_SIMPLE
+}