diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5ee1b25a0..4a806361c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -139,7 +139,7 @@ jobs:
- name: "clang"
cc: "clang"
cxx: "clang++"
- buildargs: ""
+ buildargs: "-DENABLE_FUZZ=ON"
name: "${{ matrix.os }} (${{ matrix.config.name }})"
runs-on: ${{ matrix.os }}
@@ -305,7 +305,7 @@ jobs:
- name: "clang"
cc: "clang"
cxx: "clang++"
- buildargs: "-DENABLE_MAN=ON -DENABLE_DOC=ON -DENABLE_TESTS=ON"
+ buildargs: "-DENABLE_MAN=ON -DENABLE_DOC=ON -DENABLE_TESTS=ON -DENABLE_FUZZ=ON"
name: "rockylinux-9 (${{ matrix.config.name }})"
runs-on: ${{ matrix.os }}
@@ -354,7 +354,7 @@ jobs:
- name: "clang"
cc: "clang"
cxx: "clang++"
- buildargs: "-DENABLE_MAN=ON -DENABLE_DOC=ON -DENABLE_TESTS=ON"
+ buildargs: "-DENABLE_MAN=ON -DENABLE_DOC=ON -DENABLE_TESTS=ON -DENABLE_FUZZ=ON"
name: "freebsd-14 (${{ matrix.config.name }})"
runs-on: ${{ matrix.os }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f658c9bc..6aa6e3b0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
- irc: add support strikethrough text in IRC messages ([#2248](https://github.com/weechat/weechat/issues/2248))
- buflist: add variables `${number_zero}` and `${number_zero2}` (zero-padded buffer number)
+- tests: add fuzz testing ([#1462](https://github.com/weechat/weechat/issues/1462))
### Fixed
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ba0ccbef4..685b27998 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -131,6 +131,7 @@ option(ENABLE_MAN "Enable build of man page" OFF)
option(ENABLE_DOC "Enable build of documentation" OFF)
option(ENABLE_DOC_INCOMPLETE "Enable incomplete doc" OFF)
option(ENABLE_TESTS "Enable tests" OFF)
+option(ENABLE_FUZZ "Enable fuzz testing" OFF)
option(ENABLE_CODE_COVERAGE "Enable code coverage" OFF)
# code coverage
@@ -276,19 +277,7 @@ endif()
add_subdirectory(src)
add_subdirectory(doc)
-
-if(ENABLE_TESTS)
- find_package(CppUTest)
- if(CPPUTEST_FOUND)
- enable_testing()
- add_subdirectory(tests)
- else()
- message(SEND_ERROR "CppUTest not found")
- endif()
-else()
- enable_testing()
- add_test(NAME notests COMMAND true)
-endif()
+add_subdirectory(tests)
configure_file(config.h.cmake config.h @ONLY)
diff --git a/doc/en/weechat_dev.en.adoc b/doc/en/weechat_dev.en.adoc
index 4bcb9558c..98f428b27 100644
--- a/doc/en/weechat_dev.en.adoc
+++ b/doc/en/weechat_dev.en.adoc
@@ -91,6 +91,8 @@ The main WeeChat directories are:
| typing/ | Typing plugin.
| xfer/ | Xfer plugin (IRC DCC file/chat).
| tests/ | Tests.
+| fuzz/ | Fuzz testing.
+| core/ | Fuzz testing for core functions.
| unit/ | Unit tests.
| core/ | Unit tests for core functions.
| hook/ | Unit tests for hook functions.
@@ -415,6 +417,12 @@ WeeChat "core" is located in following directories:
|===
| Path/file | Description
| tests/ | Root of tests.
+| fuzz/ | Root of fuzz testing.
+| core/ | Root of fuzz testing for core.
+| calc-fuzzer.c | Fuzz testing: calculation of expressions.
+| crypto-fuzzer.c | Fuzz testing: cryptographic functions.
+| string-fuzzer.c | Fuzz testing: strings.
+| utf8-fuzzer.c | Fuzz testing: UTF-8.
| unit/ | Root of unit tests.
| tests.cpp | Program used to run all tests.
| tests-record.cpp | Record and search in messages displayed.
diff --git a/doc/fr/weechat_dev.fr.adoc b/doc/fr/weechat_dev.fr.adoc
index 41f8b180a..5c32ea0b6 100644
--- a/doc/fr/weechat_dev.fr.adoc
+++ b/doc/fr/weechat_dev.fr.adoc
@@ -92,6 +92,8 @@ Les répertoires principaux de WeeChat sont :
| typing/ | Extension Typing.
| xfer/ | Extension Xfer (IRC DCC fichier/discussion).
| tests/ | Tests.
+| fuzz/ | Fuzzing (tests à données aléatoires).
+| core/ | Fuzzing pour les fonctions du cœur.
| unit/ | Tests unitaires.
| core/ | Tests unitaires pour les fonctions du cœur.
| hook/ | Tests unitaires pour les fonctions hook.
@@ -416,6 +418,12 @@ Le cœur de WeeChat est situé dans les répertoires suivants :
|===
| Chemin/fichier | Description
| tests/ | Racine des tests.
+| fuzz/ | Racine du fuzzing (tests à données aléatoires).
+| core/ | Racine du fuzzing pour le cœur.
+| calc-fuzzer.c | Fuzzing : calcul d'expressions.
+| crypto-fuzzer.c | Fuzzing : fonctions cryptographiques.
+| string-fuzzer.c | Fuzzing : chaînes.
+| utf8-fuzzer.c | Fuzzing : UTF-8.
| unit/ | Racine des tests unitaires.
| tests.cpp | Programme utilisé pour lancer tous les tests.
| tests-record.cpp | Enregistrement et recherche dans les messages affichés.
diff --git a/doc/ja/weechat_dev.ja.adoc b/doc/ja/weechat_dev.ja.adoc
index 661356ccd..fdba82bd1 100644
--- a/doc/ja/weechat_dev.ja.adoc
+++ b/doc/ja/weechat_dev.ja.adoc
@@ -93,6 +93,10 @@ qweechat::
| typing/ | typing プラグイン
| xfer/ | xfer (IRC DCC ファイル/チャット)
| tests/ | テスト
+// TRANSLATION MISSING
+| fuzz/ | Fuzz testing.
+// TRANSLATION MISSING
+| core/ | Fuzz testing for core functions.
| unit/ | 単体テスト
| core/ | コア関数の単体テスト
// TRANSLATION MISSING
@@ -456,6 +460,18 @@ WeeChat "core" は以下のディレクトリに配置されています:
|===
| パス/ファイル名 | 説明
| tests/ | テスト用のルートディレクトリ
+// TRANSLATION MISSING
+| fuzz/ | Root of fuzz testing.
+// TRANSLATION MISSING
+| core/ | Root of fuzz testing for core.
+// TRANSLATION MISSING
+| calc-fuzzer.c | Fuzz testing: calculation of expressions.
+// TRANSLATION MISSING
+| crypto-fuzzer.c | Fuzz testing: cryptographic functions.
+// TRANSLATION MISSING
+| string-fuzzer.c | Fuzz testing: 文字列
+// TRANSLATION MISSING
+| utf8-fuzzer.c | Fuzz testing: UTF-8.
| unit/ | 単体テスト用のルートディレクトリ
| tests.cpp | 全テストの実行時に使われるプログラム
// TRANSLATION MISSING
diff --git a/doc/sr/weechat_dev.sr.adoc b/doc/sr/weechat_dev.sr.adoc
index 0c1d50455..7b98588f5 100644
--- a/doc/sr/weechat_dev.sr.adoc
+++ b/doc/sr/weechat_dev.sr.adoc
@@ -91,6 +91,10 @@ qweechat::
| typing/ | Typing додатак.
| xfer/ | Xfer додатак (IRC DCC фајл/разговор).
| tests/ | Тестови.
+// TRANSLATION MISSING
+| fuzz/ | Fuzz testing.
+// TRANSLATION MISSING
+| core/ | Fuzz testing for core functions.
| unit/ | Unit тестови.
| core/ | Unit тестови за функције језгра.
// TRANSLATION MISSING
@@ -422,6 +426,18 @@ WeeChat „језгро” се налази у следећим директо
|===
| Путања/фајл | Опис
| tests/ | Корен тестова.
+// TRANSLATION MISSING
+| fuzz/ | Root of fuzz testing.
+// TRANSLATION MISSING
+| core/ | Root of fuzz testing for core.
+// TRANSLATION MISSING
+| calc-fuzzer.c | Fuzz testing: калкулација израза.
+// TRANSLATION MISSING
+| crypto-fuzzer.c | Fuzz testing: криптографске функције.
+// TRANSLATION MISSING
+| string-fuzzer.c | Fuzz testing: стрингови.
+// TRANSLATION MISSING
+| utf8-fuzzer.c | Fuzz testing: UTF-8.
| unit/ | Корен unit тестова.
| tests.cpp | Програм који се користи за извршавање свих тестова.
| tests-record.cpp | Бележење и претрага у приказаним порукама.
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 333a3beb1..8b506853a 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -19,6 +19,19 @@
# along with WeeChat. If not, see .
#
-if(ENABLE_TESTS)
- add_subdirectory(unit)
+if(ENABLE_FUZZ)
+ add_subdirectory(fuzz)
+endif()
+
+if(ENABLE_TESTS)
+ find_package(CppUTest)
+ if(CPPUTEST_FOUND)
+ enable_testing()
+ add_subdirectory(unit)
+ else()
+ message(SEND_ERROR "CppUTest not found")
+ endif()
+else()
+ enable_testing()
+ add_test(NAME notests COMMAND true)
endif()
diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt
new file mode 100644
index 000000000..c2728aef8
--- /dev/null
+++ b/tests/fuzz/CMakeLists.txt
@@ -0,0 +1,101 @@
+#
+# SPDX-FileCopyrightText: 2025 Sébastien Helleu
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of WeeChat, the extensible chat client.
+#
+# WeeChat 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.
+#
+# WeeChat 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 WeeChat. If not, see .
+#
+
+if(NOT DEFINED ENV{LIB_FUZZING_ENGINE})
+ set(ENV{LIB_FUZZING_ENGINE} "-fsanitize=address,fuzzer")
+endif()
+
+remove_definitions(-DHAVE_CONFIG_H)
+include_directories(
+ ${PROJECT_BINARY_DIR}
+ ${PROJECT_SOURCE_DIR}
+ ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+if(NOT CYGWIN)
+ add_definitions(-fPIC)
+endif()
+
+if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND HAVE_BACKTRACE)
+ list(APPEND EXTRA_LIBS "execinfo")
+endif()
+
+if(${CMAKE_SYSTEM_NAME} STREQUAL "SunOS")
+ list(APPEND EXTRA_LIBS "socket" "nsl")
+endif()
+
+if(${CMAKE_SYSTEM_NAME} STREQUAL "Haiku")
+ list(APPEND EXTRA_LIBS "network")
+endif()
+
+if(NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Haiku")
+ list(APPEND EXTRA_LIBS "pthread")
+endif()
+
+if(ICONV_LIBRARY)
+ list(APPEND EXTRA_LIBS ${ICONV_LIBRARY})
+endif()
+
+if(LIBINTL_LIBRARY)
+ list(APPEND EXTRA_LIBS ${LIBINTL_LIBRARY})
+endif()
+
+list(APPEND EXTRA_LIBS "m")
+
+list(APPEND EXTRA_LIBS ${ZLIB_LIBRARY})
+
+if(ENABLE_ZSTD)
+ list(APPEND EXTRA_LIBS ${LIBZSTD_LDFLAGS})
+endif()
+
+if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+ # link with resolv lib on macOS
+ list(APPEND EXTRA_LIBS "resolv")
+endif()
+
+list(APPEND FUZZ_TARGET_LINK
+ weechat_core
+ weechat_plugins
+ weechat_gui_common
+ weechat_gui_headless
+ weechat_ncurses_fake
+ ${EXTRA_LIBS}
+ ${ZLIB_LIBRARY}
+ ${LIBZSTD_LDFLAGS}
+ -rdynamic
+)
+list(APPEND FUZZ_TARGET_DEPS
+ weechat_core
+ weechat_plugins
+ weechat_gui_common
+ weechat_gui_headless
+ weechat_ncurses_fake
+)
+
+# fuzz targets
+set(FUZZ_CORE_TARGETS calc crypto string utf8)
+
+foreach(fuzz_target ${FUZZ_CORE_TARGETS})
+ add_executable(weechat_core_${fuzz_target}_fuzzer core/${fuzz_target}-fuzzer.c)
+ target_link_libraries(weechat_core_${fuzz_target}_fuzzer ${FUZZ_TARGET_LINK})
+ set_target_properties(weechat_core_${fuzz_target}_fuzzer PROPERTIES LINK_FLAGS "$ENV{LIB_FUZZING_ENGINE}")
+ add_dependencies(weechat_core_${fuzz_target}_fuzzer ${FUZZ_TARGET_DEPS})
+endforeach()
diff --git a/tests/fuzz/core/calc-fuzzer.c b/tests/fuzz/core/calc-fuzzer.c
new file mode 100644
index 000000000..b7e0619a8
--- /dev/null
+++ b/tests/fuzz/core/calc-fuzzer.c
@@ -0,0 +1,45 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Sébastien Helleu
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * This file is part of WeeChat, the extensible chat client.
+ *
+ * WeeChat 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.
+ *
+ * WeeChat 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 WeeChat. If not, see .
+ */
+
+/* Fuzz testing on WeeChat core calc functions */
+
+#include
+#include
+#include
+#include
+
+#include "src/core/core-calc.h"
+
+int
+LLVMFuzzerTestOneInput (const uint8_t *data, size_t size)
+{
+ char *str;
+
+ str = (char *)malloc (size + 1);
+ memcpy (str, data, size);
+ str[size] = '\0';
+
+ free (calc_expression (str));
+
+ free (str);
+
+ return 0;
+}
diff --git a/tests/fuzz/core/crypto-fuzzer.c b/tests/fuzz/core/crypto-fuzzer.c
new file mode 100644
index 000000000..00c5aead5
--- /dev/null
+++ b/tests/fuzz/core/crypto-fuzzer.c
@@ -0,0 +1,71 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Sébastien Helleu
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * This file is part of WeeChat, the extensible chat client.
+ *
+ * WeeChat 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.
+ *
+ * WeeChat 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 WeeChat. If not, see .
+ */
+
+/* Fuzz testing on WeeChat core crypto functions */
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "src/core/core-crypto.h"
+
+extern char *weecrypto_hash_algo_string[];
+extern int weecrypto_hash_algo[];
+
+int
+LLVMFuzzerTestOneInput (const uint8_t *data, size_t size)
+{
+ char *str, hash[1024], *result;
+ int i, hash_size, key_size, salt_size;
+
+ str = (char *)malloc (size + 1);
+ memcpy (str, data, size);
+ str[size] = '\0';
+
+ key_size = (size > 8) ? 8 : size;
+ for (i = 0; weecrypto_hash_algo_string[i]; i++)
+ {
+ weecrypto_hash (data, size, weecrypto_hash_algo[i], hash, &hash_size);
+ weecrypto_hmac (data, key_size, data, size, weecrypto_hash_algo[i], hash, &hash_size);
+ }
+
+ salt_size = (size > 8) ? 8 : size;
+ weecrypto_hash_pbkdf2 (data, size, GCRY_MD_SHA1, data, salt_size, 100, hash, &hash_size);
+ weecrypto_hash_pbkdf2 (data, size, GCRY_MD_SHA256, data, salt_size, 100, hash, &hash_size);
+ weecrypto_hash_pbkdf2 (data, size, GCRY_MD_SHA512, data, salt_size, 100, hash, &hash_size);
+
+ result = weecrypto_totp_generate (str, 1746358623, 6);
+ if (result && result[0])
+ assert (weecrypto_totp_validate (str, 1746358623, 0, result));
+ free (result);
+ result = weecrypto_totp_generate (str, 1746358623, 12);
+ if (result && result[0])
+ assert (weecrypto_totp_validate (str, 1746358623, 0, result));
+ free (result);
+
+ free (str);
+
+ return 0;
+}
diff --git a/tests/fuzz/core/string-fuzzer.c b/tests/fuzz/core/string-fuzzer.c
new file mode 100644
index 000000000..19d0522e1
--- /dev/null
+++ b/tests/fuzz/core/string-fuzzer.c
@@ -0,0 +1,228 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Sébastien Helleu
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * This file is part of WeeChat, the extensible chat client.
+ *
+ * WeeChat 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.
+ *
+ * WeeChat 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 WeeChat. If not, see .
+ */
+
+/* Fuzz testing on WeeChat core string functions */
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "src/core/core-config.h"
+#include "src/core/core-string.h"
+#include "src/plugins/weechat-plugin.h"
+
+regex_t global_regex;
+
+int
+LLVMFuzzerInitialize(int *argc, char ***argv)
+{
+ (void) argc;
+ (void) argv;
+
+ config_weechat_init ();
+
+ regcomp (&global_regex, "a.*", 0);
+
+ return 0;
+}
+
+char *
+callback_replace (void *data, const char *text)
+{
+ (void) data;
+ (void) text;
+
+ return strdup ("z");
+}
+
+int
+LLVMFuzzerTestOneInput (const uint8_t *data, size_t size)
+{
+ char *str, *str2, **str_dyn, *result, *masks[3] = { "a*", "b*", NULL};
+ char **argv, *buffer;
+ const char *name;
+ int argc, flags, num_tags, priority;
+ regex_t regex;
+ int rc;
+
+ str = (char *)malloc (size + 1);
+ memcpy (str, data, size);
+ str[size] = '\0';
+
+ free (string_strndup (str, size / 2));
+
+ free (string_cut (str, size / 2, 0, 0, "…"));
+ free (string_cut (str, size / 2, 1, 0, "…"));
+ free (string_cut (str, size / 2, 0, 1, "…"));
+ free (string_cut (str, size / 2, 1, 1, "…"));
+
+ free (string_reverse (str));
+
+ free (string_reverse_screen (str));
+
+ free (string_repeat (str, 2));
+
+ free (string_tolower (str));
+
+ free (string_toupper (str));
+
+ free (string_tolower_range (str, 13));
+
+ free (string_toupper_range (str, 13));
+
+ string_strcmp (str, str);
+
+ string_strncmp (str, str, size / 2);
+
+ string_strcasecmp (str, str);
+
+ string_strcasecmp_range (str, str, 13);
+
+ string_strncasecmp (str, str, size / 2);
+
+ string_strcmp_ignore_chars (str, str, "abcd", 0);
+ string_strcmp_ignore_chars (str, str, "abcd", 1);
+
+ string_strcasestr (str, str);
+
+ string_match (str, "a*", 0);
+ string_match (str, "a*", 1);
+
+ string_match_list (str, (const char **)masks, 0);
+ string_match_list (str, (const char **)masks, 1);
+
+ free (string_expand_home (str));
+
+ free (string_eval_path_home (str, NULL, NULL, NULL));
+
+ free (string_remove_quotes (str, "'\""));
+
+ free (string_strip (str, 0, 0, "abcdef"));
+ free (string_strip (str, 0, 1, "abcdef"));
+ free (string_strip (str, 1, 0, "abcdef"));
+ free (string_strip (str, 1, 1, "abcdef"));
+
+ free (string_convert_escaped_chars (str));
+
+ string_is_whitespace_char (str);
+
+ string_is_word_char_highlight (str);
+
+ string_is_word_char_input (str);
+
+ free (string_mask_to_regex (str));
+
+ string_regex_flags (str, 0, NULL);
+ string_regex_flags (str, 0, &flags);
+
+ rc = string_regcomp (®ex, str, REG_ICASE | REG_NOSUB);
+ if (rc == 0)
+ regfree (®ex);
+ str2 = (char *)malloc (16 + size + 1);
+ snprintf (str2, 16 + size + 1, "(?ins)%s", str);
+ rc = string_regcomp (®ex, str2, REG_ICASE | REG_NOSUB);
+ if (rc == 0)
+ regfree (®ex);
+ free (str2);
+
+ string_has_highlight (str, "a");
+
+ string_has_highlight_regex_compiled (str, &global_regex);
+
+ string_has_highlight_regex (str, "a.*");
+
+ free (string_replace (str, "a", "b"));
+
+ free (string_replace_regex (str, &global_regex, "b", '$', &callback_replace, NULL));
+
+ free (string_translate_chars (str, "abc", "def"));
+
+ argv = string_split (str, "/", NULL, 0, 0, &argc);
+ string_free_split (argv);
+ argv = string_split (str, "/", " ", 0, 0, &argc);
+ string_free_split (argv);
+ flags = WEECHAT_STRING_SPLIT_STRIP_LEFT
+ | WEECHAT_STRING_SPLIT_STRIP_RIGHT
+ | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS
+ | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS;
+ argv = string_split (str, "/", " ", flags, 0, &argc);
+ result = string_rebuild_split_string ((const char **)argv, "/", -1, -1);
+ if (argv && result)
+ assert (strcmp (str, result) == 0);
+ free (result);
+ string_free_split (argv);
+
+ string_free_split_shared (string_split_shared (str, "/", " ", flags, 0, &argc));
+
+ string_free_split_command (string_split_command (str, ';'));
+
+ string_free_split_tags (string_split_tags (str, &num_tags));
+
+ free (string_iconv (0, "utf-8", "iso-8859-1", str));
+
+ free (string_iconv_to_internal ("iso-8859-1", str));
+
+ free (string_iconv_from_internal ("iso-8859-1", str));
+
+ free (string_format_size (size));
+
+ string_parse_size (str);
+
+ buffer = malloc ((size * 4) + 8 + 1);
+ string_base16_encode (str, size, buffer);
+ string_base16_decode (str, buffer);
+ string_base32_encode (str, size, buffer);
+ string_base32_decode (str, buffer);
+ string_base64_encode (0, str, size, buffer);
+ string_base64_encode (1, str, size, buffer);
+ string_base64_decode (0, str, buffer);
+ string_base64_decode (1, str, buffer);
+ free (buffer);
+
+ free (string_hex_dump (str, size, 16, "<", ">"));
+
+ string_is_command_char (str);
+
+ string_input_for_buffer (str);
+
+ string_get_common_bytes_count (str, str);
+
+ string_levenshtein (str, str, 0);
+ string_levenshtein (str, str, 1);
+
+ string_get_priority_and_name (str, &priority, &name, 0);
+
+ string_shared_free ((char *)string_shared_get (str));
+
+ str_dyn = string_dyn_alloc (1);
+ string_dyn_copy (str_dyn, str);
+ string_dyn_concat (str_dyn, str, -1);
+ string_dyn_free (str_dyn, 1);
+
+ string_concat ("/", str, str, NULL);
+
+ free (str);
+
+ return 0;
+}
diff --git a/tests/fuzz/core/utf8-fuzzer.c b/tests/fuzz/core/utf8-fuzzer.c
new file mode 100644
index 000000000..9d8bac23e
--- /dev/null
+++ b/tests/fuzz/core/utf8-fuzzer.c
@@ -0,0 +1,103 @@
+/*
+ * SPDX-FileCopyrightText: 2025 Sébastien Helleu
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * This file is part of WeeChat, the extensible chat client.
+ *
+ * WeeChat 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.
+ *
+ * WeeChat 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 WeeChat. If not, see .
+ */
+
+/* Fuzz testing on WeeChat core UTF-8 functions */
+
+#include
+#include
+#include
+#include
+
+#include "src/core/core-config.h"
+#include "src/core/core-utf8.h"
+
+int
+LLVMFuzzerInitialize(int *argc, char ***argv)
+{
+ (void) argc;
+ (void) argv;
+
+ config_weechat_init ();
+
+ return 0;
+}
+
+int
+LLVMFuzzerTestOneInput (const uint8_t *data, size_t size)
+{
+ char *str, *str2, utf8_char[5], *error;
+ size_t i;
+
+ str = (char *)malloc (size + 1);
+ memcpy (str, data, size);
+ str[size] = '\0';
+
+ utf8_has_8bits (str);
+
+ utf8_is_valid (str, size, &error);
+
+ str2 = strdup (str);
+ utf8_normalize (str2, '?');
+ free (str2);
+
+ for (i = 0; i < 5; i++)
+ {
+ if (size >= i)
+ {
+ utf8_prev_char (str, str + i);
+ utf8_beginning_of_line (str, str + i);
+ }
+ }
+
+ utf8_next_char (str);
+
+ utf8_end_of_line (str);
+
+ utf8_char_int (str);
+
+ utf8_int_string (utf8_char_int (str), utf8_char);
+
+ utf8_char_size (str);
+
+ utf8_strlen (str);
+
+ utf8_strnlen (str, size / 2);
+
+ utf8_char_size_screen (str);
+
+ utf8_strlen_screen (str);
+
+ if (size > 4)
+ {
+ utf8_add_offset (str, 1);
+ utf8_real_pos (str, 1);
+ utf8_pos (str, 1);
+ }
+
+ free (utf8_strndup (str, size / 2));
+
+ if (size > 4)
+ utf8_strncpy (utf8_char, str, 1);
+
+ free (str);
+
+ return 0;
+}
diff --git a/tests/fuzz/weechat_core_calc_fuzzer.dict b/tests/fuzz/weechat_core_calc_fuzzer.dict
new file mode 100644
index 000000000..5bc3edb81
--- /dev/null
+++ b/tests/fuzz/weechat_core_calc_fuzzer.dict
@@ -0,0 +1,24 @@
+# SPDX-FileCopyrightText: 2025 Sébastien Helleu
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"0"
+"1"
+"2"
+"3"
+"4"
+"5"
+"6"
+"7"
+"8"
+"9"
+"."
+"("
+")"
+"+"
+"-"
+"*"
+"/"
+"//"
+"%"
+"**"