From d3ef4ab234572e7ffac5918f6aad9c0e5b82a2f9 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max.kellermann@gmail.com>
Date: Fri, 5 Jul 2024 18:37:03 +0200
Subject: [PATCH] net/FormatAddress: new library to replace ToString.cxx

This library writes to a caller-specified buffer instead of
allocating a std::string which can be faster by avoiding heap
allocations.
---
 src/net/FormatAddress.cxx | 141 ++++++++++++++++++++++++++++++++++++++
 src/net/FormatAddress.hxx |  36 ++++++++++
 src/net/meson.build       |   1 +
 3 files changed, 178 insertions(+)
 create mode 100644 src/net/FormatAddress.cxx
 create mode 100644 src/net/FormatAddress.hxx

diff --git a/src/net/FormatAddress.cxx b/src/net/FormatAddress.cxx
new file mode 100644
index 000000000..20a9ef33b
--- /dev/null
+++ b/src/net/FormatAddress.cxx
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// Copyright CM4all GmbH
+// author: Max Kellermann <mk@cm4all.com>
+
+#include "FormatAddress.hxx"
+#include "Features.hxx"
+#include "SocketAddress.hxx"
+#include "IPv4Address.hxx"
+
+#include <algorithm>
+#include <cassert>
+#include <cstring>
+
+#ifdef _WIN32
+#include <ws2tcpip.h>
+#else
+#include <netdb.h>
+#ifdef HAVE_TCP
+#include <netinet/in.h>
+#endif
+#endif
+
+#ifdef HAVE_UN
+#include <sys/un.h>
+#endif
+
+#ifdef HAVE_UN
+
+static bool
+LocalToString(std::span<char> buffer, std::string_view raw) noexcept
+{
+	if (raw.empty())
+		return false;
+
+	if (raw.size() >= buffer.size())
+		/* truncate to the buffer size */
+		raw = raw.substr(0, buffer.size() - 1);
+
+	if (raw.front() != '\0' && raw.back() == '\0')
+		/* don't convert the null terminator of a non-abstract socket
+		   to a '@' */
+		raw.remove_suffix(1);
+
+	*std::copy(raw.begin(), raw.end(), buffer.begin()) = '\0';
+
+	/* replace all null bytes with '@'; this also handles abstract
+	   addresses (Linux specific) */
+	const auto result = buffer.first(raw.size());
+	std::replace(result.begin(), result.end(), '\0', '@');
+
+	return true;
+}
+
+#endif
+
+bool
+ToString(std::span<char> buffer, SocketAddress address) noexcept
+{
+	if (address.IsNull() || address.GetSize() == 0)
+		return false;
+
+#ifdef HAVE_UN
+	if (address.GetFamily() == AF_LOCAL)
+		/* return path of local socket */
+		return LocalToString(buffer, address.GetLocalRaw());
+#endif
+
+#if defined(HAVE_IPV6) && defined(IN6_IS_ADDR_V4MAPPED)
+	IPv4Address ipv4_buffer;
+	if (address.IsV4Mapped())
+		address = ipv4_buffer = address.UnmapV4();
+#endif
+
+	char serv[NI_MAXSERV];
+	int ret = getnameinfo(address.GetAddress(), address.GetSize(),
+			      buffer.data(), buffer.size(),
+			      serv, sizeof(serv),
+			      NI_NUMERICHOST | NI_NUMERICSERV);
+	if (ret != 0)
+		return false;
+
+	if (serv[0] != 0 && (serv[0] != '0' || serv[1] != 0)) {
+#ifdef HAVE_IPV6
+		if (address.GetFamily() == AF_INET6) {
+			/* enclose IPv6 address in square brackets */
+
+			std::size_t length = strlen(buffer.data());
+			if (length + 4 >= buffer.size())
+				/* no more room */
+				return false;
+
+			memmove(buffer.data() + 1, buffer.data(), length);
+			buffer[0] = '[';
+			buffer[++length] = ']';
+			buffer[++length] = 0;
+		}
+#endif
+
+		if (strlen(buffer.data()) + 1 + strlen(serv) >= buffer.size())
+			/* no more room */
+			return false;
+
+		strcat(buffer.data(), ":");
+		strcat(buffer.data(), serv);
+	}
+
+	return true;
+}
+
+const char *
+ToString(std::span<char> buffer, SocketAddress address,
+	 const char *fallback) noexcept
+{
+	return ToString(buffer, address)
+		? buffer.data()
+		: fallback;
+}
+
+bool
+HostToString(std::span<char> buffer, SocketAddress address) noexcept
+{
+	if (address.IsNull() || address.GetSize() == 0)
+		return false;
+
+#ifdef HAVE_UN
+	if (address.GetFamily() == AF_LOCAL)
+		/* return path of local socket */
+		return LocalToString(buffer, address.GetLocalRaw());
+#endif
+
+#if defined(HAVE_IPV6) && defined(IN6_IS_ADDR_V4MAPPED)
+	IPv4Address ipv4_buffer;
+	if (address.IsV4Mapped())
+		address = ipv4_buffer = address.UnmapV4();
+#endif
+
+	return getnameinfo(address.GetAddress(), address.GetSize(),
+			   buffer.data(), buffer.size(),
+			   nullptr, 0,
+			   NI_NUMERICHOST | NI_NUMERICSERV) == 0;
+}
diff --git a/src/net/FormatAddress.hxx b/src/net/FormatAddress.hxx
new file mode 100644
index 000000000..a5fabc7dd
--- /dev/null
+++ b/src/net/FormatAddress.hxx
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: BSD-2-Clause
+// Copyright CM4all GmbH
+// author: Max Kellermann <mk@cm4all.com>
+
+#pragma once
+
+#include <cstddef>
+#include <span>
+
+class SocketAddress;
+
+/**
+ * Generates the string representation of a #SocketAddress into the
+ * specified buffer.
+ *
+ * @return true on success
+ */
+bool
+ToString(std::span<char> buffer, SocketAddress address) noexcept;
+
+/**
+ * Like ToString() above, but return the string pointer (or on error:
+ * return the given fallback pointer).
+ */
+const char *
+ToString(std::span<char> buffer, SocketAddress address,
+	 const char *fallback) noexcept;
+
+/**
+ * Generates the string representation of a #SocketAddress into the
+ * specified buffer, without the port number.
+ *
+ * @return true on success
+ */
+bool
+HostToString(std::span<char> buffer, SocketAddress address) noexcept;
diff --git a/src/net/meson.build b/src/net/meson.build
index 1faa5ed54..cd817850a 100644
--- a/src/net/meson.build
+++ b/src/net/meson.build
@@ -41,6 +41,7 @@ endif
 net = static_library(
   'net',
   net_sources,
+  'FormatAddress.cxx',
   'ToString.cxx',
   'HostParser.cxx',
   'Resolver.cxx',