// Copyright 2012-2025 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Adapted from server/util.go in the NATS server Go source. using System.Net; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace ZB.MOM.NatsNet.Server.Internal; /// /// General-purpose server utility methods. /// Mirrors server/util.go. /// public static class ServerUtilities { // Semver validation regex — mirrors semVerRe in const.go. private static readonly Regex SemVerRe = new( @"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", RegexOptions.Compiled); // ------------------------------------------------------------------------- // Version helpers // ------------------------------------------------------------------------- /// /// Parses a semver string into major/minor/patch components. /// Returns an error if the string is not a valid semver. /// Mirrors versionComponents. /// public static (int major, int minor, int patch, Exception? err) VersionComponents(string version) { var m = SemVerRe.Match(version); if (!m.Success) return (0, 0, 0, new InvalidOperationException("invalid semver")); if (!int.TryParse(m.Groups[1].Value, out var major) || !int.TryParse(m.Groups[2].Value, out var minor) || !int.TryParse(m.Groups[3].Value, out var patch)) return (-1, -1, -1, new InvalidOperationException("invalid semver component")); return (major, minor, patch, null); } /// /// Returns (true, nil) if is at least major.minor.patch. /// Mirrors versionAtLeastCheckError. /// public static (bool ok, Exception? err) VersionAtLeastCheckError( string version, int emajor, int eminor, int epatch) { var (major, minor, patch, err) = VersionComponents(version); if (err != null) return (false, err); if (major > emajor) return (true, null); if (major == emajor && minor > eminor) return (true, null); if (major == emajor && minor == eminor && patch >= epatch) return (true, null); return (false, null); } /// /// Returns true if is at least major.minor.patch. /// Mirrors versionAtLeast. /// public static bool VersionAtLeast(string version, int emajor, int eminor, int epatch) { var (ok, _) = VersionAtLeastCheckError(version, emajor, eminor, epatch); return ok; } // ------------------------------------------------------------------------- // Integer parsing helpers (used for NATS protocol parsing) // ------------------------------------------------------------------------- /// /// Parses a decimal positive integer from ASCII bytes. /// Returns -1 on error or if the input contains non-digit characters. /// Mirrors parseSize. /// public static int ParseSize(ReadOnlySpan d) { const int MaxParseSizeLen = 9; // 999M if (d.IsEmpty || d.Length > MaxParseSizeLen) return -1; var n = 0; foreach (var dec in d) { if (dec < '0' || dec > '9') return -1; n = n * 10 + (dec - '0'); } return n; } /// /// Parses a decimal positive int64 from ASCII bytes. /// Returns -1 on error. /// Mirrors parseInt64. /// public static long ParseInt64(ReadOnlySpan d) { if (d.IsEmpty) return -1; long n = 0; foreach (var dec in d) { if (dec < '0' || dec > '9') return -1; n = n * 10 + (dec - '0'); } return n; } // ------------------------------------------------------------------------- // Duration / network helpers // ------------------------------------------------------------------------- /// /// Converts float64 seconds to a . /// Mirrors secondsToDuration. /// public static TimeSpan SecondsToDuration(double seconds) => TimeSpan.FromSeconds(seconds); /// /// Splits "host:port" into components, using /// when no port (or port 0 / -1) is present. /// Mirrors parseHostPort. /// public static (string host, int port, Exception? err) ParseHostPort(string hostPort, int defaultPort) { if (string.IsNullOrEmpty(hostPort)) return ("", -1, new InvalidOperationException("no hostport specified")); // Try splitting; if port is missing, append the default and retry. string host, sPort; try { var ep = ParseEndpoint(hostPort); host = ep.host; sPort = ep.port; } catch { try { var ep = ParseEndpoint($"{hostPort}:{defaultPort}"); host = ep.host; sPort = ep.port; } catch (Exception ex) { return ("", -1, ex); } } if (!int.TryParse(sPort.Trim(), out var port)) return ("", -1, new InvalidOperationException($"invalid port: {sPort}")); if (port == 0 || port == -1) port = defaultPort; return (host.Trim(), port, null); } private static (string host, string port) ParseEndpoint(string hostPort) { // net.SplitHostPort equivalent — handles IPv6 [::1]:port if (hostPort.StartsWith('[')) { var closeIdx = hostPort.IndexOf(']'); if (closeIdx < 0 || closeIdx + 1 >= hostPort.Length || hostPort[closeIdx + 1] != ':') throw new InvalidOperationException($"missing port in address {hostPort}"); return (hostPort[1..closeIdx], hostPort[(closeIdx + 2)..]); } var lastColon = hostPort.LastIndexOf(':'); if (lastColon < 0) throw new InvalidOperationException($"missing port in address {hostPort}"); var host = hostPort[..lastColon]; var port = hostPort[(lastColon + 1)..]; // Reject bare IPv6 addresses (multiple colons without brackets). if (host.Contains(':')) throw new InvalidOperationException($"too many colons in address {hostPort}"); return (host, port); } /// /// Returns true if two instances represent the same URL. /// Mirrors urlsAreEqual. /// public static bool UrlsAreEqual(Uri? u1, Uri? u2) => u1 == u2 || (u1 != null && u2 != null && u1.ToString() == u2.ToString()); // ------------------------------------------------------------------------- // Comma formatting // ------------------------------------------------------------------------- /// /// Formats an int64 with comma thousands separators. /// Mirrors comma in util.go. /// public static string Comma(long v) { if (v == long.MinValue) return "-9,223,372,036,854,775,808"; var sign = ""; if (v < 0) { sign = "-"; v = -v; } var parts = new string[7]; var j = parts.Length - 1; while (v > 999) { var part = (v % 1000).ToString(); parts[j--] = part.Length switch { 2 => "0" + part, 1 => "00" + part, _ => part }; v /= 1000; } parts[j] = v.ToString(); return sign + string.Join(",", parts.Skip(j)); } // ------------------------------------------------------------------------- // TCP helpers // ------------------------------------------------------------------------- /// /// Creates a TCP listener with keepalives disabled (NATS server default). /// Mirrors natsListen. /// public static System.Net.Sockets.TcpListener NatsListen(string address, int port) { // .NET TcpListener does not set keepalive by default; the socket can be // further configured after creation if needed. var listener = new System.Net.Sockets.TcpListener(IPAddress.Parse(address), port); return listener; } /// /// Opens a TCP connection with the given timeout and keepalives disabled. /// Mirrors natsDialTimeout. /// public static async Task NatsDialTimeoutAsync( string host, int port, TimeSpan timeout) { var client = new System.Net.Sockets.TcpClient(); // Disable keepalive to match Go 1.12 behavior. client.Client.SetSocketOption( System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.KeepAlive, false); using var cts = new CancellationTokenSource(timeout); await client.ConnectAsync(host, port, cts.Token); return client; } /// /// Parity wrapper for Go natsDialTimeout. /// Accepts a network label (tcp/tcp4/tcp6) and host:port address. /// public static Task NatsDialTimeout( string network, string address, TimeSpan timeout) { if (!string.Equals(network, "tcp", StringComparison.OrdinalIgnoreCase) && !string.Equals(network, "tcp4", StringComparison.OrdinalIgnoreCase) && !string.Equals(network, "tcp6", StringComparison.OrdinalIgnoreCase)) throw new NotSupportedException($"unsupported network: {network}"); var (host, port, err) = ParseHostPort(address, defaultPort: 0); if (err != null || port <= 0) throw new InvalidOperationException($"invalid dial address: {address}", err); return NatsDialTimeoutAsync(host, port, timeout); } // ------------------------------------------------------------------------- // URL redaction // ------------------------------------------------------------------------- /// /// Returns a copy of where any URL that /// contains a password has its password replaced with "xxxxx". /// Mirrors redactURLList. /// public static Uri[] RedactUrlList(Uri[] unredacted) { var r = new Uri[unredacted.Length]; var needCopy = false; for (var i = 0; i < unredacted.Length; i++) { var u = unredacted[i]; if (u?.UserInfo?.Contains(':') == true) { needCopy = true; var ui = u.UserInfo; var colon = ui.IndexOf(':'); var username = ui[..colon]; var b = new UriBuilder(u) { Password = "xxxxx", UserName = username }; r[i] = b.Uri; } else { r[i] = u!; } } return needCopy ? r : unredacted; } /// /// Returns the URL string with the password component redacted ("xxxxx"). /// Returns the original string if no password is present or it cannot be parsed. /// Mirrors redactURLString. /// public static string RedactUrlString(string raw) { if (!raw.Contains('@')) return raw; if (!Uri.TryCreate(raw, UriKind.Absolute, out var u)) return raw; if (!u.UserInfo.Contains(':')) return raw; var colon = u.UserInfo.IndexOf(':'); var username = u.UserInfo[..colon]; var b = new UriBuilder(u) { Password = "xxxxx", UserName = username }; var result = b.Uri.ToString(); // UriBuilder adds a trailing slash for authority-only URLs; strip it if the input had none. if (!raw.EndsWith('/') && result.EndsWith('/')) result = result[..^1]; return result; } /// /// Returns the Host part of each URL in the list. /// Mirrors getURLsAsString. /// public static string[] GetUrlsAsString(Uri[] urls) { var result = new string[urls.Length]; for (var i = 0; i < urls.Length; i++) result[i] = urls[i].Authority; // host:port return result; } // ------------------------------------------------------------------------- // RefCountedUrlSet wrappers (Go parity mapping) // ------------------------------------------------------------------------- /// /// Parity wrapper for . /// Mirrors refCountedUrlSet.addUrl. /// public static bool AddUrl(RefCountedUrlSet urlSet, string urlStr) { ArgumentNullException.ThrowIfNull(urlSet); return urlSet.AddUrl(urlStr); } /// /// Parity wrapper for . /// Mirrors refCountedUrlSet.removeUrl. /// public static bool RemoveUrl(RefCountedUrlSet urlSet, string urlStr) { ArgumentNullException.ThrowIfNull(urlSet); return urlSet.RemoveUrl(urlStr); } /// /// Parity wrapper for . /// Mirrors refCountedUrlSet.getAsStringSlice. /// public static string[] GetAsStringSlice(RefCountedUrlSet urlSet) { ArgumentNullException.ThrowIfNull(urlSet); return urlSet.GetAsStringSlice(); } // ------------------------------------------------------------------------- // INFO helpers // ------------------------------------------------------------------------- /// /// Serialises into an INFO line (INFO {...}\r\n). /// Mirrors generateInfoJSON. /// public static byte[] GenerateInfoJSON(global::ZB.MOM.NatsNet.Server.ServerInfo info) { var json = JsonSerializer.Serialize(info); return Encoding.UTF8.GetBytes($"INFO {json}\r\n"); } // ------------------------------------------------------------------------- // Copy helpers // ------------------------------------------------------------------------- /// /// Returns a copy of , or null if src is empty. /// Mirrors copyBytes. /// public static byte[]? CopyBytes(byte[]? src) { if (src == null || src.Length == 0) return null; var dst = new byte[src.Length]; src.CopyTo(dst, 0); return dst; } /// /// Returns a copy of , or null if src is null. /// Mirrors copyStrings. /// public static string[]? CopyStrings(string[]? src) { if (src == null) return null; var dst = new string[src.Length]; src.CopyTo(dst, 0); return dst; } // ------------------------------------------------------------------------- // Parallel task queue // ------------------------------------------------------------------------- /// /// Creates a bounded channel onto which tasks can be posted for parallel /// execution across a pool of dedicated threads. Close the returned channel /// to signal workers to stop (after queued items complete). /// Mirrors parallelTaskQueue. /// public static System.Threading.Channels.ChannelWriter CreateParallelTaskQueue(int maxParallelism = 0) { var mp = maxParallelism <= 0 ? Environment.ProcessorCount : Math.Max(Environment.ProcessorCount, maxParallelism); var channel = System.Threading.Channels.Channel.CreateBounded(mp); for (var i = 0; i < mp; i++) { Task.Run(async () => { await foreach (var fn in channel.Reader.ReadAllAsync()) fn(); }); } return channel.Writer; } /// /// Parity wrapper for . /// Mirrors parallelTaskQueue. /// public static System.Threading.Channels.ChannelWriter ParallelTaskQueue(int maxParallelism = 0) => CreateParallelTaskQueue(maxParallelism); } // ------------------------------------------------------------------------- // RefCountedUrlSet (mirrors refCountedUrlSet map[string]int in util.go) // ------------------------------------------------------------------------- /// /// A reference-counted set of URL strings used for gossip URL management. /// Mirrors refCountedUrlSet in server/util.go. /// public sealed class RefCountedUrlSet { private readonly Dictionary _map = new(); /// /// Adds . Returns true if it was added for the first time. /// Mirrors refCountedUrlSet.addUrl. /// public bool AddUrl(string urlStr) { _map.TryGetValue(urlStr, out var count); _map[urlStr] = count + 1; return count == 0; } /// /// Decrements the reference count for . /// Returns true if this was the last reference (entry removed). /// Mirrors refCountedUrlSet.removeUrl. /// public bool RemoveUrl(string urlStr) { if (!_map.TryGetValue(urlStr, out var count)) return false; if (count == 1) { _map.Remove(urlStr); return true; } _map[urlStr] = count - 1; return false; } /// /// Returns the unique URL strings currently in the set. /// Mirrors refCountedUrlSet.getAsStringSlice. /// public string[] GetAsStringSlice() => [.. _map.Keys]; }