From 910134acd2ad9700aa02c3054bc1f4920e939679 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Gomez?= <bip@kyoshiro.org>
Date: Thu, 6 Jan 2022 13:43:18 +0100
Subject: [PATCH 02/03] Move /BIP commands to bip_strcat{,f}_fit() memccpy()
 wrapper instead of snprintf()

---
 src/bip.c  | 380 ++++++++++++++++++++++++++++++++---------------------
 src/util.c |  63 +++++++++
 src/util.h |   2 +
 3 files changed, 295 insertions(+), 150 deletions(-)

diff --git a/src/bip.c b/src/bip.c
index 813b407..9828342 100644
--- a/src/bip.c
+++ b/src/bip.c
@@ -1144,12 +1144,14 @@ void check_rlimits()
 
 #define RET_STR_LEN 256
 #define LINE_SIZE_LIM 70
+
 void adm_print_connection(struct link_client *ic, struct link *lnk,
 		struct bipuser *bu)
 {
 	hash_iterator_t lit;
-	char buf[RET_STR_LEN + 1];
-	int t_written = 0;
+	char buf[LINE_SIZE_LIM + 1];
+	char *bufpos = buf;
+	size_t remaining = LINE_SIZE_LIM;
 
 	if (!bu)
 		bu = lnk->user;
@@ -1160,136 +1162,195 @@ void adm_print_connection(struct link_client *ic, struct link *lnk,
 		(lnk->connect_nick ? lnk->connect_nick : bu->default_nick),
 		(lnk->username ? lnk->username : bu->default_username));
 
-	t_written = snprintf(buf, RET_STR_LEN, "  Options:");
-	if (t_written >= RET_STR_LEN)
-		goto noroom;
+	bufpos = bip_strcat_fit(&remaining, bufpos, "  Options:");
+	// This should not happen, unless LINE_SIZE_LIM is too low
+	if (!bufpos)
+		goto limittoolow;
+
 	if (lnk->follow_nick) {
-		t_written += snprintf(buf + t_written,
-			RET_STR_LEN - t_written, " follow_nick");
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
+		bufpos = bip_strcat_fit(&remaining, bufpos, " follow_nick");
+		if (!bufpos) {
+			buf[LINE_SIZE_LIM] = 0;
+			bip_notify(ic, "%s", buf);
+			remaining = LINE_SIZE_LIM;
+			bufpos = bip_strcat_fit(&remaining, bufpos, "      follow_nick");
+			if (!bufpos)
+				goto limittoolow;
+		}
 	}
 	if (lnk->ignore_first_nick) {
-		t_written += snprintf(buf + t_written,
-			RET_STR_LEN - t_written, " ignore_first_nick");
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
+		bufpos = bip_strcat_fit(&remaining, bufpos, " ignore_first_nick");
+		if (!bufpos) {
+			buf[LINE_SIZE_LIM] = 0;
+			bip_notify(ic, "%s", buf);
+			remaining = LINE_SIZE_LIM;
+			bufpos = bip_strcat_fit(&remaining, bufpos, "      ignore_first_nick");
+			if (!bufpos)
+				goto limittoolow;
+		}
 	}
 	if (lnk->away_nick) {
-		t_written += snprintf(buf + t_written,
-			RET_STR_LEN - t_written, " away_nick=%s",
-			lnk->away_nick);
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
+		bufpos = bip_strcatf_fit(&remaining, bufpos, " away_nick=%s", lnk->away_nick);
+		if (!bufpos) {
+			buf[LINE_SIZE_LIM] = 0;
+			bip_notify(ic, "%s", buf);
+			remaining = LINE_SIZE_LIM;
+			bufpos = bip_strcatf_fit(&remaining, bufpos, "      away_nick=%s", lnk->away_nick);
+			if (!bufpos)
+				goto limittoolow;
+		}
 	}
 	if (lnk->no_client_away_msg) {
-		t_written += snprintf(buf + t_written,
-			RET_STR_LEN - t_written, " no_client_away_msg=%s",
-			lnk->no_client_away_msg);
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
+		bufpos = bip_strcatf_fit(&remaining, bufpos, " no_client_away_msg=%s", lnk->no_client_away_msg);
+		if (!bufpos) {
+			buf[LINE_SIZE_LIM] = 0;
+			bip_notify(ic, "%s", buf);
+			remaining = LINE_SIZE_LIM;
+			bufpos = bip_strcatf_fit(&remaining, bufpos, "      no_client_away_msg=%s", lnk->no_client_away_msg);
+			if (!bufpos)
+				goto limittoolow;
+		}
 	}
 	if (lnk->vhost) {
-		t_written += snprintf(buf + t_written,
-			RET_STR_LEN - t_written, " vhost=%s",
-			lnk->vhost);
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
+		bufpos = bip_strcatf_fit(&remaining, bufpos, " vhost=%s", lnk->vhost);
+		if (!bufpos) {
+			buf[LINE_SIZE_LIM] = 0;
+			bip_notify(ic, "%s", buf);
+			remaining = LINE_SIZE_LIM;
+			bufpos = bip_strcatf_fit(&remaining, bufpos, "      vhost=%s", lnk->vhost);
+			if (!bufpos)
+				goto limittoolow;
+		}
 	}
 	if (lnk->bind_port) {
-		t_written += snprintf(buf + t_written,
-			RET_STR_LEN - t_written, " bind_port=%u",
-			lnk->bind_port);
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
-	}
-noroom: /* that means the line is larger that RET_STR_LEN. We're not likely to
-	   even read such a long line */
-	buf[RET_STR_LEN] = 0;
+		bufpos = bip_strcatf_fit(&remaining, bufpos, " bind_port=%s", lnk->bind_port);
+		if (!bufpos) {
+			buf[LINE_SIZE_LIM] = 0;
+			bip_notify(ic, "%s", buf);
+			remaining = LINE_SIZE_LIM;
+			bufpos = bip_strcatf_fit(&remaining, bufpos, "      bind_port=%s", lnk->bind_port);
+			if (!bufpos)
+				goto limittoolow;
+		}
+	}
+	buf[LINE_SIZE_LIM] = 0;
+	bip_notify(ic, "%s", buf);
+	remaining = LINE_SIZE_LIM;
+	bufpos = buf;
+
+	list_iterator_t itocs;
+	for (list_it_init(&lnk->on_connect_send, &itocs);
+				list_it_item(&itocs); ) {
+		bufpos = bip_strcatf_fit(&remaining, bufpos, "%s",
+				(char *)list_it_item(&itocs));
+		if (!bufpos) {
+			// if oversized, print and reset
+			buf[LINE_SIZE_LIM] = 0;
+			bip_notify(ic, "%s", buf);
+			remaining = LINE_SIZE_LIM;
+			bufpos = buf;
+			continue;
+		} else {
+			// if ok, go to next item
+			list_it_next(&itocs);
+		}
+
+        }
+	buf[LINE_SIZE_LIM] = 0;
 	bip_notify(ic, "%s", buf);
+	remaining = LINE_SIZE_LIM;
+	bufpos = buf;
 
-	// TODO: on_connect_send
 
 	// TODO : check channels struct
-	t_written = snprintf(buf, RET_STR_LEN, "  Channels (* with key, ` no backlog)");
-	if (t_written >= RET_STR_LEN)
-		goto noroomchan;
+	bufpos = bip_strcat_fit(&remaining, bufpos, "  Channels (* with key, ` no backlog)");
+	if (!bufpos)
+		goto limittoolow;
+
 	for (hash_it_init(&lnk->chan_infos, &lit); hash_it_item(&lit);
 			hash_it_next(&lit)) {
 		struct chan_info *ch = hash_it_item(&lit);
 
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written, " %s%s%s",
+		bufpos = bip_strcatf_fit(&remaining, bufpos, "%s%s%s",
 			ch->name, (ch->key ? "*" : ""),
 			(ch->backlog ? "" : "`"));
-		if (t_written > LINE_SIZE_LIM) {
-			buf[RET_STR_LEN] = 0;
+		if (!bufpos) {
+			buf[LINE_SIZE_LIM] = 0;
 			bip_notify(ic, "%s", buf);
-			t_written = 0;
+			remaining = LINE_SIZE_LIM;
+			bufpos = buf;
 		}
 	}
-noroomchan:
-	buf[RET_STR_LEN] = 0;
+
+	buf[LINE_SIZE_LIM] = 0;
 	bip_notify(ic, "%s", buf);
+	remaining = LINE_SIZE_LIM;
+	bufpos = buf;
 
-	t_written = snprintf(buf, RET_STR_LEN, "  Status: ");
-	if (t_written >= RET_STR_LEN)
-		goto noroomstatus;
+	bufpos = bip_strcat_fit(&remaining, bufpos, "  Status: ");
+	if (!bufpos)
+		goto limittoolow;
 	switch (lnk->s_state) {
 	case  IRCS_NONE:
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
-			"not started");
-		if (t_written >= RET_STR_LEN)
-			goto noroomstatus;
+		bufpos = bip_strcat_fit(&remaining, bufpos, "not started");
+		if (!bufpos)
+			goto limittoolow;
 		break;
 	case  IRCS_CONNECTING:
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
+		bufpos = bip_strcatf_fit(&remaining, bufpos,
 			"connecting... attempts: %d, last: %s",
 			lnk->s_conn_attempt,
 			hrtime(lnk->last_connection_attempt));
-		if (t_written >= RET_STR_LEN)
+		if (!bufpos)
 			goto noroomstatus;
 		break;
 	case  IRCS_CONNECTED:
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
-			"connected !");
-		if (t_written >= RET_STR_LEN)
-			goto noroomstatus;
+		bufpos = bip_strcat_fit(&remaining, bufpos, "connected !");
+		if (!bufpos)
+			goto limittoolow;
 		break;
 	case  IRCS_WAS_CONNECTED:
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
+		bufpos = bip_strcatf_fit(&remaining, bufpos,
 			"disconnected, attempts: %d, last: %s",
 			lnk->s_conn_attempt,
 			hrtime(lnk->last_connection_attempt));
-		if (t_written >= RET_STR_LEN)
+		if (!bufpos)
 			goto noroomstatus;
 		break;
 	case  IRCS_RECONNECTING:
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
+		bufpos = bip_strcatf_fit(&remaining, bufpos,
 			"reconnecting... attempts: %d, last: %s",
 			lnk->s_conn_attempt,
 			hrtime(lnk->last_connection_attempt));
-		if (t_written >= RET_STR_LEN)
+		if (!bufpos)
 			goto noroomstatus;
 		break;
 	case  IRCS_TIMER_WAIT:
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
+		bufpos = bip_strcatf_fit(&remaining, bufpos,
 			"waiting to reconnect, attempts: %d, last: %s",
 			lnk->s_conn_attempt,
 			hrtime(lnk->last_connection_attempt));
-		if (t_written >= RET_STR_LEN)
+		if (!bufpos)
 			goto noroomstatus;
 		break;
 	default:
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
-			"unknown");
-		if (t_written >= RET_STR_LEN)
-			goto noroomstatus;
+		bufpos = bip_strcat_fit(&remaining, bufpos, "unknown");
+		if (!bufpos)
+			goto limittoolow;
 		break;
 		// s_conn_attempt recon_timer last_connection_attempt
 	}
-noroomstatus:
-	buf[RET_STR_LEN] = 0;
+	buf[LINE_SIZE_LIM] = 0;
 	bip_notify(ic, "%s", buf);
+	return;
+noroomstatus:
+	buf[LINE_SIZE_LIM] = 0;
+	bip_notify(ic, "%stoo long to print", buf);
+	return;
+limittoolow:
+	bip_notify(ic, "cannot print connection, LINE_SIZE_LIM(%d) "
+		       "is too low (please recompile)", LINE_SIZE_LIM);
+	return;
 }
 
 void adm_list_all_links(struct link_client *ic)
@@ -1325,8 +1386,6 @@ void adm_list_all_connections(struct link_client *ic)
 void adm_info_user(struct link_client *ic, const char *name)
 {
 	struct bipuser *u;
-	char buf[RET_STR_LEN + 1];
-	int t_written = 0;
 
 	bip_notify(ic, "-- User '%s' info", name);
 	u = hash_get(&_bip->users, name);
@@ -1335,20 +1394,7 @@ void adm_info_user(struct link_client *ic, const char *name)
 		return;
 	}
 
-	t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
-			      "user: %s", u->name);
-	if (t_written >= RET_STR_LEN)
-		goto noroom;
-	if (u->admin) {
-		t_written += snprintf(buf + t_written, RET_STR_LEN - t_written,
-				", is bip admin");
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
-	}
-
-noroom:
-	buf[RET_STR_LEN] = 0;
-	bip_notify(ic, "%s", buf);
+	bip_notify(ic, "user: %s%s", u->name, (u->admin ? ", is bip admin" : ""));
 
 #ifdef HAVE_LIBSSL
 	if (u->ssl_check_store) {
@@ -1387,48 +1433,64 @@ void adm_list_users(struct link_client *ic)
 {
 	hash_iterator_t it;
 	hash_iterator_t lit;
-	char buf[RET_STR_LEN + 1];
+	char buf[LINE_SIZE_LIM + 1];
+	size_t remaining = LINE_SIZE_LIM;
 
 	bip_notify(ic, "-- User list");
 	for (hash_it_init(&_bip->users, &it); hash_it_item(&it);
 			hash_it_next(&it)) {
 		struct bipuser *u = hash_it_item(&it);
 		int first = 1;
-		int t_written = 0;
+		char *bufpos = buf;
 
-		buf[RET_STR_LEN] = 0;
-		t_written += snprintf(buf, RET_STR_LEN, "* %s%s:", u->name,
+		bufpos = bip_strcatf_fit(&remaining, bufpos, "* %s%s:", u->name,
 				(u->admin ? "(admin)": ""));
-		if (t_written >= RET_STR_LEN)
-			goto noroom;
-		for (hash_it_init(&u->connections, &lit); hash_it_item(&lit);
-				hash_it_next(&lit)) {
+		// this should not happen or LINE_SIZE_LIM is really low...
+		if (!bufpos)
+			goto limittoolow;
+		for (hash_it_init(&u->connections, &lit); hash_it_item(&lit); ) {
 			struct link *lnk = hash_it_item(&lit);
 			if (first) {
 				first = 0;
 			} else {
-				t_written += snprintf(buf + t_written,
-						RET_STR_LEN - t_written, ",");
-				if (t_written >= RET_STR_LEN)
-					goto noroom;
+				bufpos = bip_strcat_fit(&remaining, bufpos, ",");
+				// if this is too long for a comma, print and prefix with spaces
+				if (!bufpos) {
+					buf[LINE_SIZE_LIM] = 0;
+					bip_notify(ic, "%s", buf);
+					remaining = LINE_SIZE_LIM;
+					bufpos = bip_strcat_fit(&remaining, buf, "     ");;
+					// this should not happen or LINE_SIZE_LIM is really low...
+					if (!bufpos)
+						goto limittoolow;
+				}
 			}
 
-			t_written += snprintf(buf + t_written,
-					RET_STR_LEN - t_written,
-					" %s", lnk->name);
-			if (t_written >= RET_STR_LEN)
-				goto noroom;
-			if (t_written > LINE_SIZE_LIM) {
-				buf[RET_STR_LEN] = 0;
+			bufpos = bip_strcatf_fit(&remaining, bufpos, " %s", lnk->name);
+			if (!bufpos) {
+				// if this is too long, print and reset
+				buf[LINE_SIZE_LIM] = 0;
 				bip_notify(ic, "%s", buf);
-				t_written = 0;
+				remaining = LINE_SIZE_LIM;
+				bufpos = bip_strcat_fit(&remaining, buf, "     ");;
+				// this should not happen or LINE_SIZE_LIM is really low...
+				if (!bufpos)
+					goto limittoolow;
+			} else {
+				// if all good, go to next entry
+				hash_it_next(&lit);
 			}
 		}
-noroom:
-		buf[RET_STR_LEN] = 0;
+		buf[LINE_SIZE_LIM] = 0;
 		bip_notify(ic, "%s", buf);
+		remaining = LINE_SIZE_LIM;
+		bufpos = buf;
 	}
 	bip_notify(ic, "-- End of User list");
+	return;
+limittoolow:
+	bip_notify(ic, "cannot print users, LINE_SIZE_LIM(%d) "
+			"is too low (please recompile)", LINE_SIZE_LIM);
 }
 
 void adm_list_networks(struct link_client *ic)
@@ -1440,38 +1502,53 @@ void adm_list_networks(struct link_client *ic)
 	for (hash_it_init(&_bip->networks, &it); hash_it_item(&it);
 			hash_it_next(&it)) {
 		struct network *n = hash_it_item(&it);
-		int t_written = 0;
 		int i;
+		char *bufpos = buf;
+		size_t remaining = RET_STR_LEN;
 
-		buf[RET_STR_LEN] = 0;
 #ifdef HAVE_LIBSSL
 		if (n->ssl) {
-			t_written += snprintf(buf, RET_STR_LEN, "- %s*:",
+			bufpos = bip_strcatf_fit(&remaining, bufpos, "- %s*:",
 					n->name);
-			if (t_written >= RET_STR_LEN)
-				goto noroom;
 		} else {
 #endif
-			t_written += snprintf(buf, RET_STR_LEN, "- %s:", n->name);
-			if (t_written >= RET_STR_LEN)
-				goto noroom;
+			bufpos = bip_strcatf_fit(&remaining, bufpos, "- %s:",
+					n->name);
 #ifdef HAVE_LIBSSL
 		}
 #endif
-		for (i = 0; i < n->serverc; i++) {
+		// if we've reached max length, print name and reset
+		// honestly, this should not happen, but for the sake of cleanliness...
+		if (!bufpos) {
+#ifdef HAVE_LIBSSL
+			if (n->ssl) {
+				bip_notify(ic, "- %s*:", n->name);
+			} else {
+#endif
+				bip_notify(ic, "- %s:", n->name);
+#ifdef HAVE_LIBSSL
+			}
+#endif
+			bufpos = buf;
+			remaining = RET_STR_LEN;
+		}
+
+		for (i = 0; i < n->serverc; ) {
 			struct server *serv = i+n->serverv;
-			t_written += snprintf(buf + t_written, RET_STR_LEN
-				- t_written, " %s:%d", serv->host,
-				serv->port);
-			if (t_written >= RET_STR_LEN)
-				goto noroom;
-			if (t_written > LINE_SIZE_LIM) {
+			bufpos = bip_strcatf_fit(&remaining, bufpos, " %s:%d",
+					serv->host, serv->port);
+			if (!bufpos) {
+				// if line is too long, print and reset
 				buf[RET_STR_LEN] = 0;
 				bip_notify(ic, "%s", buf);
-				t_written = 0;
+				remaining = RET_STR_LEN;
+				bufpos = buf;
+				i--;
+			} else {
+				// if ok, go to next server
+				i++;
 			}
 		}
-noroom:
 		buf[RET_STR_LEN] = 0;
 		bip_notify(ic, "%s", buf);
 	}
@@ -1717,8 +1794,9 @@ void set_on_connect_send(struct link_client *ic, char *val)
 void adm_on_connect_send(struct link_client *ic, struct line *line,
 		unsigned int privmsg)
 {
+	size_t remaining = ON_CONNECT_MAX_STRSIZE;
 	char buf[ON_CONNECT_MAX_STRSIZE];
-	int t_written = 0;
+	char *bufpos = buf;
 	int i;
 
 	if (!line) {
@@ -1726,30 +1804,32 @@ void adm_on_connect_send(struct link_client *ic, struct line *line,
 		return;
 	}
 
-	if (irc_line_includes(line, 2))
+	if (!irc_line_includes(line, 2)) {
+		mylog(LOG_DEBUG, "[%s] not enough parameters on /BIP on_connect_send",
+				LINK(ic)->user->name);
 		return;
+	}
 
 	for (i = privmsg + 2; i < irc_line_count(line); i++) {
-		if (t_written) {
-			t_written += snprintf(buf,
-					ON_CONNECT_MAX_STRSIZE - 1 - t_written,
-					" %s", irc_line_elem(line, i));
-			if (t_written >= ON_CONNECT_MAX_STRSIZE)
-				goto noroom;
-		} else {
-			t_written = snprintf(buf, ON_CONNECT_MAX_STRSIZE - 1,
-					"%s", irc_line_elem(line, i));
-			if (t_written >= ON_CONNECT_MAX_STRSIZE)
-				goto noroom;
+		mylog(LOG_DEBUG, "[%s] processing item %d, remaining %ld, %s",
+				LINK(ic)->user->name, i, remaining, buf);
+		if ((unsigned int)i > privmsg + 2)
+			bufpos = bip_strcatf_fit(&remaining, bufpos, " %s",
+					irc_line_elem(line, i));
+		else
+			bufpos = bip_strcat_fit(&remaining, bufpos,
+					(char *)irc_line_elem(line, i));
+		mylog(LOG_DEBUG, "[%s] processed item %d, remaining %ld, %s",
+				LINK(ic)->user->name, i, remaining, buf);
+		if (!bufpos) {
+			bip_notify(ic, "on connect send string too big, not changing.");
+			return;
 		}
 	}
-ok:
+
 	buf[ON_CONNECT_MAX_STRSIZE - 1] = 0;
 	set_on_connect_send(ic, buf);
 	return;
-noroom:
-	bip_notify(ic, "on connect send string too big, truncated");
-	goto ok;
 }
 
 void adm_away_nick(struct link_client *ic, const char *val)
@@ -1924,7 +2004,7 @@ int adm_bip(bip_t *bip, struct link_client *ic, struct line *line, int privmsg)
 
 	mylog(LOG_INFO, "/BIP %s from %s", irc_line_elem(line, privmsg + 1),
 			LINK(ic)->user->name);
-	if (strcasecmp(irc_line_elem(line, privmsg + 1), "RELOAD") == 0) {
+	if (irc_line_elem_case_equals(line, privmsg + 1, "RELOAD")) {
 		if (!admin) {
 			bip_notify(ic, "-- You're not allowed to reload bip");
 			return OK_FORGET;
@@ -1932,7 +2012,7 @@ int adm_bip(bip_t *bip, struct link_client *ic, struct line *line, int privmsg)
 		bip_notify(ic, "-- Reloading bip...");
 		bip->reloading_client = ic;
 		sighup = 1;
-	} else if (strcasecmp(irc_line_elem(line, privmsg + 1), "LIST") == 0) {
+	} else if (irc_line_elem_case_equals(line, privmsg + 1, "LIST")) {
 		if (irc_line_count(line) != privmsg + 3) {
 			bip_notify(ic, "-- LIST command needs one argument");
 			return OK_FORGET;
@@ -1956,10 +2036,10 @@ int adm_bip(bip_t *bip, struct link_client *ic, struct line *line, int privmsg)
 		} else {
 			bip_notify(ic, "-- Invalid LIST request");
 		}
-	} else if (strcasecmp(irc_line_elem(line, privmsg + 1), "INFO") == 0) {
-		if (!irc_line_includes(line, privmsg + 2)) {
-			bip_notify(ic, "-- INFO command needs at least one "
-					"argument");
+	} else if (irc_line_elem_case_equals(line, privmsg + 1, "INFO")) {
+		if (!irc_line_includes(line, privmsg + 3)) {
+			bip_notify(ic, "-- INFO command needs at least two "
+					"arguments");
 			return OK_FORGET;
 		}
 
diff --git a/src/util.c b/src/util.c
index 43450b3..7e756fb 100644
--- a/src/util.c
+++ b/src/util.c
@@ -81,6 +81,69 @@ char *bip_strdup(const char *str)
 	return r;
 }
 
+char *bip_strcat_fit(size_t *remaining, char *str, char *str2)
+{
+	char *res;
+
+	if (!remaining || !str || !str2) {
+		mylog(LOG_DEBUGVERB, "bip_strcat_fit: got NULL pointer");
+		return NULL;
+	}
+
+	res = memccpy(str, str2, '\0', *remaining);
+	if (!res) {
+		mylog(LOG_DEBUGTOOMUCH, "bip_strcat_fit: memccpy() failed, remaining %lu",
+				*remaining);
+		return NULL;
+	}
+
+	res--;
+	if (res < str) {
+		mylog(LOG_DEBUG, "bip_strcat_fit: memccpy res < str");
+		return NULL;
+	}
+	(*remaining) -= (size_t)(res - str);
+	return res;
+}
+
+char *bip_strcatf_fit(size_t *remaining, char *str, char *fmt, ...)
+{
+	va_list ap;
+	char str2[*remaining];
+	size_t written;
+	char *res = NULL;
+
+	if (!remaining || !str || !fmt) {
+		mylog(LOG_DEBUGVERB, "bip_strcatf_fit: NULL pointer");
+		return NULL;
+	}
+
+	va_start(ap, fmt);
+	written = vsnprintf(str2, *remaining, fmt, ap);
+	if (written >= *remaining) {
+		mylog(LOG_DEBUGVERB, "bip_strcatf_fit,vsnprintf: no space left");
+		goto end;
+	}
+
+	res = memccpy(str, str2, '\0', *remaining);
+	if (!res) {
+		mylog(LOG_DEBUGTOOMUCH, "bip_strcatf_fit: memccpy() failed, remaining %lu",
+				*remaining);
+		goto end;
+	}
+
+	if (res < str) {
+		mylog(LOG_DEBUG, "bip_strcatf_fit: memccpy res < str");
+		goto end;
+	}
+	res--;
+	(*remaining) -= (size_t)(res - str);
+
+end:
+	va_end(ap);
+	return res;
+}
+
 /*
  * <nick> ::= <letter> { <letter> | <number> | <special> }
  * <special> ::= '-' | '[' | ']' | '\' | '`' | '^' | '{' | '}'
diff --git a/src/util.h b/src/util.h
index aa37cae..600d4b9 100644
--- a/src/util.h
+++ b/src/util.h
@@ -175,6 +175,8 @@ void *bip_malloc(size_t size);
 void *bip_calloc(size_t nmemb, size_t size);
 void *bip_realloc(void *ptr, size_t size);
 char *bip_strdup(const char *str);
+char *bip_strcat_fit(size_t *remaining, char *str, char *str2);
+char *bip_strcatf_fit(size_t *remaining, char *str, char *str2, ...);
 #define array_each(a, idx, ptr) for ((idx) = 0; \
 		(idx) < (a)->elemc && (((ptr) = array_get((a), (idx))) || 1); \
 		(idx)++)
-- 
2.30.2

