From bd27baa44141e91d39373882c00b196d0a5c0585 Mon Sep 17 00:00:00 2001 From: Rob Norris Date: Sun, 17 Jan 2016 12:58:59 +0000 Subject: [PATCH] Remove own copy of MD5 hash code and use a library implementation (libnettle) Selected libnettle due to wide support and standard license. --- .gitignore | 1 + configure.ac | 16 +++ mingw-viking.spec.in | 2 + mingw64-viking.spec.in | 2 + src/Makefile.am | 1 + src/dialog.c | 3 + src/md5_hash.c | 61 ++++++++++ src/md5_hash.h | 32 +++++ src/thumbnails.c | 254 +-------------------------------------- test/Makefile.am | 13 ++ test/check_md5_hash.sh | 13 ++ test/test_md5_hash.c | 23 ++++ win32/installer-mingw.sh | 1 + 13 files changed, 169 insertions(+), 253 deletions(-) create mode 100644 src/md5_hash.c create mode 100644 src/md5_hash.h create mode 100755 test/check_md5_hash.sh create mode 100644 test/test_md5_hash.c diff --git a/.gitignore b/.gitignore index b4ab06de..01cd3927 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,7 @@ src/misc/*.o /test/gpx2gpx /test/degrees_converter /test/test_vikgotoxmltool +/test/test_md5_hash /test/test_metatile /test/test_coord_conversion /test/test_babel diff --git a/configure.ac b/configure.ac index bd80c677..8390461f 100644 --- a/configure.ac +++ b/configure.ac @@ -380,6 +380,21 @@ case $ac_cv_enable_zip in esac AM_CONDITIONAL([ZIP], [test x$ac_cv_enable_zip = xyes]) +# ATM only for MD5 Hashing which is currently only used for filename of image thumbnails +AC_ARG_ENABLE(nettle, AC_HELP_STRING([--enable-nettle], + [enable MD5 Hash support (default is enable)]), + [ac_cv_enable_nettle=$enableval], + [ac_cv_enable_nettle=yes]) +AC_CACHE_CHECK([whether to enable MD5 Hash support], + [ac_cv_enable_nettle], [ac_cv_enable_nettle=yes]) +case $ac_cv_enable_nettle in + yes) + AC_CHECK_HEADER([nettle/md5-compat.h],[],[AC_MSG_ERROR([nettle/md5-compat.h is needed but not found - you will need to install package 'nettle-dev' or similar. The feature can be disabled with --disable-nettle])]) + AC_CHECK_LIB([nettle], [main], [], [AC_MSG_ERROR([libnettle is needed but not found.])]) + ;; +esac +AM_CONDITIONAL([MD5_HASH], [test x$ac_cv_enable_nettle = xyes]) + # Mapnik rendering layer AC_ARG_ENABLE(mapnik, AC_HELP_STRING([--enable-mapnik], [enable Mapnik (default is enable)]), @@ -505,6 +520,7 @@ bzip2 Support : $ac_cv_enable_bzip2 File Magic Support : $ac_cv_enable_magic MBTiles Support (SQLite3) : $ac_cv_enable_mbtiles Zip File Support (with libzip) : $ac_cv_enable_zip +MD5 Hash Support (with libnettle): $ac_cv_enable_nettle Mapnik Rendering Support (C++) : $ac_cv_enable_mapnik Size of map cache (in memory) : ${VIK_CONFIG_MAPCACHE_SIZE} Age of tiles (in seconds) : ${VIK_CONFIG_DEFAULT_TILE_AGE} diff --git a/mingw-viking.spec.in b/mingw-viking.spec.in index 11aee9bf..fcc5684f 100644 --- a/mingw-viking.spec.in +++ b/mingw-viking.spec.in @@ -43,6 +43,7 @@ BuildRequires: mingw32-libmagic1 BuildRequires: mingw32-libbz2-1 BuildRequires: mingw32-sqlite-devel BuildRequires: mingw32-libzip-devel +BuildRequires: mingw32-libnettle-devel # Libs for runtime (and thus also available for the NSIS installer to include the dependencies) Requires: mingw32-gtk2 @@ -52,6 +53,7 @@ Requires: mingw32-libgexiv2 Requires: mingw32-libstdc++6 Requires: mingw32-libsqlite3-0 Requires: mingw32-libzip4 +Requires: mingw32-libnettle # Currently running makensis in seperate script - so you will need it then #Requires: mingw32-cross-nsis diff --git a/mingw64-viking.spec.in b/mingw64-viking.spec.in index 1a56bc45..73a9db9a 100644 --- a/mingw64-viking.spec.in +++ b/mingw64-viking.spec.in @@ -43,6 +43,7 @@ BuildRequires: mingw64-libmagic1 BuildRequires: mingw64-libbz2-1 BuildRequires: mingw64-sqlite-devel BuildRequires: mingw64-libzip-devel +BuildRequires: mingw64-libnettle-devel # Libs for runtime (and thus also available for the NSIS installer to include the dependencies) Requires: mingw64-gtk2 @@ -52,6 +53,7 @@ Requires: mingw64-libgexiv2 Requires: mingw64-libstdc++6 Requires: mingw64-libsqlite3-0 Requires: mingw64-libzip4 +Requires: mingw64-libnettle # Currently running makensis in seperate script - so you will need it then # NB No 64bit version available #Requires: mingw32-cross-nsis diff --git a/src/Makefile.am b/src/Makefile.am index 46eef01f..65ad04f2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -103,6 +103,7 @@ libviking_a_SOURCES = \ vikutils.c vikutils.h \ toolbar.c toolbar.h toolbar.xml.h \ thumbnails.c thumbnails.h \ + md5_hash.c md5_hash.h \ background.c background.h \ vikradiogroup.c vikradiogroup.h \ vikcoord.c vikcoord.h \ diff --git a/src/dialog.c b/src/dialog.c index 05a7479f..ef0cbced 100644 --- a/src/dialog.c +++ b/src/dialog.c @@ -693,6 +693,9 @@ void a_dialog_about ( GtkWindow *parent ) #endif #ifdef HAVE_LIBMAPNIK "libmapnik", +#endif +#ifdef HAVE_LIBNETTLE + "libnettle", #endif NULL }; diff --git a/src/md5_hash.c b/src/md5_hash.c new file mode 100644 index 00000000..eaec8355 --- /dev/null +++ b/src/md5_hash.c @@ -0,0 +1,61 @@ +/* + * viking -- GPS Data and Topo Analyzer, Explorer, and Manager + * + * Copyright (C) 2016 Rob Norris + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +#include "md5_hash.h" +#include +#include +#include +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#ifdef HAVE_LIBNETTLE +#include +#endif + +/** + * Get MD5 hash of a string using a library function + */ +char *md5_hash(const char *message) +{ + char *answer = NULL; +#ifdef HAVE_LIBNETTLE + unsigned char result[16]; + MD5_CTX ctx; + MD5Init ( &ctx ); + MD5Update ( &ctx, (const unsigned char*)message, strlen(message) ); + MD5Final ( &result[0], &ctx ); + /* + g_printf ( "%s: of string '%s' is: ", __FUNCTION__, message ); + for(int i = 0; i < 16; i++) + g_printf ( "%02x", result[i]); + g_printf("\n"); + */ + // Should be nicer way of converting this but I can't think of one ATM + answer = g_strdup_printf ( "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + result[0],result[1],result[2],result[3],result[4],result[5],result[6],result[7], + result[8],result[9],result[10],result[11],result[12],result[13],result[14],result[15]); +#else + // Return something that might vaguely work in case you've disabled MD5 support + // but you should know what you've doing and uses of it (such as thumbnail caching) now won't work well + guint value = g_str_hash ( message ); + answer = g_strdup_printf ( "%d", value ); +#endif + return answer; +} diff --git a/src/md5_hash.h b/src/md5_hash.h new file mode 100644 index 00000000..2ff4f30c --- /dev/null +++ b/src/md5_hash.h @@ -0,0 +1,32 @@ +/* + * viking -- GPS Data and Topo Analyzer, Explorer, and Manager + * + * Copyright (C) 2016 Rob Norris + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +#ifndef __VIKING_MD5_H +#define __VIKING_MD5_H + +#include + +G_BEGIN_DECLS + +char *md5_hash(const char *message); + +G_END_DECLS + +#endif diff --git a/src/thumbnails.c b/src/thumbnails.c index 7880992d..58e028dc 100644 --- a/src/thumbnails.c +++ b/src/thumbnails.c @@ -41,6 +41,7 @@ #include "viking.h" #include "thumbnails.h" #include "icons/icons.h" +#include "md5_hash.h" #ifdef __CYGWIN__ #ifdef __CYGWIN_USE_BIG_TYPES__ @@ -72,7 +73,6 @@ #define PIXMAP_THUMB_SIZE 128 -static char *md5_hash(const char *message); static GdkPixbuf *save_thumbnail(const char *pathname, GdkPixbuf *full); static GdkPixbuf *child_create_thumbnail(const gchar *path); @@ -308,255 +308,3 @@ out: g_free(thumb_path); return thumb; } - -/* - * This code implements the MD5 message-digest algorithm. - * The algorithm is due to Ron Rivest. The original code was - * written by Colin Plumb in 1993, and put in the public domain. - * - * Modified to use glib datatypes. Put under GPL to simplify - * licensing for ROX-Filer. Taken from Debian's dpkg package. - * - */ - -#define md5byte unsigned char - -typedef struct _MD5Context MD5Context; - -struct _MD5Context { - guint32 buf[4]; - guint32 bytes[2]; - guint32 in[16]; -}; - -static void MD5Init(MD5Context *ctx); -static void MD5Update(MD5Context *ctx, md5byte const *buf, unsigned len); -static char *MD5Final(MD5Context *ctx); -static void MD5Transform(guint32 buf[4], guint32 const in[16]); - -#if G_BYTE_ORDER == G_BIG_ENDIAN -static void byteSwap(guint32 *buf, unsigned words) -{ - md5byte *p = (md5byte *)buf; - - do { - *buf++ = (guint32)((unsigned)p[3] << 8 | p[2]) << 16 | - ((unsigned)p[1] << 8 | p[0]); - p += 4; - } while (--words); -} -#else -#define byteSwap(buf,words) -#endif - -/* - * Start MD5 accumulation. Set bit count to 0 and buffer to mysterious - * initialization constants. - */ -static void MD5Init(MD5Context *ctx) -{ - ctx->buf[0] = 0x67452301; - ctx->buf[1] = 0xefcdab89; - ctx->buf[2] = 0x98badcfe; - ctx->buf[3] = 0x10325476; - - ctx->bytes[0] = 0; - ctx->bytes[1] = 0; -} - -/* - * Update context to reflect the concatenation of another buffer full - * of bytes. - */ -static void MD5Update(MD5Context *ctx, md5byte const *buf, unsigned len) -{ - guint32 t; - - /* Update byte count */ - - t = ctx->bytes[0]; - if ((ctx->bytes[0] = t + len) < t) - ctx->bytes[1]++; /* Carry from low to high */ - - t = 64 - (t & 0x3f); /* Space available in ctx->in (at least 1) */ - if (t > len) { - memcpy((md5byte *)ctx->in + 64 - t, buf, len); - return; - } - /* First chunk is an odd size */ - memcpy((md5byte *)ctx->in + 64 - t, buf, t); - byteSwap(ctx->in, 16); - MD5Transform(ctx->buf, ctx->in); - buf += t; - len -= t; - - /* Process data in 64-byte chunks */ - while (len >= 64) { - memcpy(ctx->in, buf, 64); - byteSwap(ctx->in, 16); - MD5Transform(ctx->buf, ctx->in); - buf += 64; - len -= 64; - } - - /* Handle any remaining bytes of data. */ - memcpy(ctx->in, buf, len); -} - -/* - * Final wrapup - pad to 64-byte boundary with the bit pattern - * 1 0* (64-bit count of bits processed, MSB-first) - * Returns the newly allocated string of the hash. - */ -static char *MD5Final(MD5Context *ctx) -{ - char *retval; - int i; - int count = ctx->bytes[0] & 0x3f; /* Number of bytes in ctx->in */ - md5byte *p = (md5byte *)ctx->in + count; - guint8 *bytes; - - /* Set the first char of padding to 0x80. There is always room. */ - *p++ = 0x80; - - /* Bytes of padding needed to make 56 bytes (-8..55) */ - count = 56 - 1 - count; - - if (count < 0) { /* Padding forces an extra block */ - memset(p, 0, count + 8); - byteSwap(ctx->in, 16); - MD5Transform(ctx->buf, ctx->in); - p = (md5byte *)ctx->in; - count = 56; - } - memset(p, 0, count); - byteSwap(ctx->in, 14); - - /* Append length in bits and transform */ - ctx->in[14] = ctx->bytes[0] << 3; - ctx->in[15] = ctx->bytes[1] << 3 | ctx->bytes[0] >> 29; - MD5Transform(ctx->buf, ctx->in); - - byteSwap(ctx->buf, 4); - - retval = g_malloc(33); - bytes = (guint8 *) ctx->buf; - for (i = 0; i < 16; i++) - sprintf(retval + (i * 2), "%02x", bytes[i]); - retval[32] = '\0'; - - return retval; -} - -# ifndef ASM_MD5 - -/* The four core functions - F1 is optimized somewhat */ - -/* #define F1(x, y, z) (x & y | ~x & z) */ -#define F1(x, y, z) (z ^ (x & (y ^ z))) -#define F2(x, y, z) F1(z, x, y) -#define F3(x, y, z) (x ^ y ^ z) -#define F4(x, y, z) (y ^ (x | ~z)) - -/* This is the central step in the MD5 algorithm. */ -#define MD5STEP(f,w,x,y,z,in,s) \ - (w += f(x,y,z) + in, w = (w<>(32-s)) + x) - -/* - * The core of the MD5 algorithm, this alters an existing MD5 hash to - * reflect the addition of 16 longwords of new data. MD5Update blocks - * the data and converts bytes into longwords for this routine. - */ -static void MD5Transform(guint32 buf[4], guint32 const in[16]) -{ - register guint32 a, b, c, d; - - a = buf[0]; - b = buf[1]; - c = buf[2]; - d = buf[3]; - - MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7); - MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12); - MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17); - MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22); - MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7); - MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12); - MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17); - MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22); - MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7); - MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12); - MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); - MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); - MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); - MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); - MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); - MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); - - MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5); - MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9); - MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); - MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20); - MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5); - MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); - MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); - MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20); - MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5); - MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); - MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14); - MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20); - MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); - MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9); - MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14); - MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); - - MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4); - MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11); - MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); - MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); - MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4); - MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11); - MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16); - MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); - MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); - MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11); - MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16); - MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23); - MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4); - MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); - MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); - MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23); - - MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6); - MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10); - MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); - MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21); - MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); - MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10); - MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); - MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21); - MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6); - MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); - MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15); - MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); - MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6); - MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); - MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15); - MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21); - - buf[0] += a; - buf[1] += b; - buf[2] += c; - buf[3] += d; -} - -# endif /* ASM_MD5 */ - -static char *md5_hash(const char *message) -{ - MD5Context ctx; - - MD5Init(&ctx); - MD5Update(&ctx, (md5byte *) message, strlen(message)); - return MD5Final(&ctx); -} diff --git a/test/Makefile.am b/test/Makefile.am index f1b7a5e5..2982f56d 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -11,12 +11,16 @@ TESTS = check_degrees_conversions.sh \ if GEOTAG TESTS += check_geotag.sh endif +if MD5_HASH +TESTS += check_md5_hash.sh +endif check_PROGRAMS = degrees_converter \ gpx2gpx \ test_vikgotoxmltool \ test_coord_conversion \ test_babel \ + test_md5_hash \ test_metatile if GEOTAG @@ -28,9 +32,13 @@ check_SCRIPTS = check_degrees_conversions.sh \ if GEOTAG check_SCRIPTS += check_geotag.sh endif +if MD5_HASH +check_SCRIPTS += check_md5_hash.sh +endif # Scripts and the test data that they use EXTRA_DIST = check_degrees_conversions.sh \ + check_md5_hash.sh \ check_metatile.sh \ metatile_example/13/0/0/250/220/0.meta \ check_geotag.sh \ @@ -74,6 +82,11 @@ test_babel_LDADD = \ $(top_builddir)/src/libviking.a \ $(LDADD) +test_md5_hash_SOURCES = test_md5_hash.c +test_md5_hash_LDADD = \ + $(top_builddir)/src/libviking.a \ + $(LDADD) + test_metatile_SOURCES = test_metatile.c test_metatile_LDADD = \ $(top_builddir)/src/libviking.a \ diff --git a/test/check_md5_hash.sh b/test/check_md5_hash.sh new file mode 100755 index 00000000..9c36acf6 --- /dev/null +++ b/test/check_md5_hash.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +test_string="Test hash of this string" + +expected_result=$(echo -n "$test_string" | md5sum | awk '{print $1}') +my_result=$(./test_md5_hash "$test_string") + +if [ "$my_result" = "$expected_result" ]; then + # Success + exit 0 +else + exit 1 +fi diff --git a/test/test_md5_hash.c b/test/test_md5_hash.c new file mode 100644 index 00000000..678085f4 --- /dev/null +++ b/test/test_md5_hash.c @@ -0,0 +1,23 @@ +// Print md5 hash of the given string +#include +#include +#include "md5_hash.h" + +int main(int argc, char *argv[]) +{ +#if !GLIB_CHECK_VERSION(2,36,0) + g_type_init (); +#endif + + if ( !argv[1] ) { + g_printerr ( "Nothing specified\n" ); + return 1; + } + + gchar *hash = md5_hash ( argv[1] ); + g_printf ( "%s\n", hash ); + g_free ( hash ); + + return 0; +} + diff --git a/win32/installer-mingw.sh b/win32/installer-mingw.sh index 60c9a893..8ed3e558 100755 --- a/win32/installer-mingw.sh +++ b/win32/installer-mingw.sh @@ -125,6 +125,7 @@ cp $MINGW_BIN/libbz*.dll $DESTINATION cp $MINGW_BIN/libmagic*.dll $DESTINATION cp $MINGW/share/misc/magic* $DESTINATION cp $MINGW_BIN/libsqlite3*.dll $DESTINATION +cp $MINGW_BIN/libnettle*.dll $DESTINATION echo Copying GPSBabel Installer mkdir $DESTINATION/Optional -- 2.39.5