namespace NATS.Server.Gateways; /// /// Maps reply subjects to gateway-prefixed forms and restores them. /// The gateway reply format is _GR_.{clusterId}.{hash}.{originalReply}. /// A legacy format _GR_.{clusterId}.{originalReply} (no hash) is also supported /// for backward compatibility. /// Go reference: gateway.go:2000-2100, gateway.go:340-380. /// public static class ReplyMapper { private const string GatewayReplyPrefix = "_GR_."; /// /// Checks whether the subject starts with the gateway reply prefix _GR_.. /// public static bool HasGatewayReplyPrefix(string? subject) => !string.IsNullOrWhiteSpace(subject) && subject.StartsWith(GatewayReplyPrefix, StringComparison.Ordinal); /// /// Computes a deterministic FNV-1a hash of the reply subject. /// Go reference: gateway.go uses SHA-256 truncated to base-62; we use FNV-1a for speed /// while maintaining determinism and good distribution. /// public static long ComputeReplyHash(string replyTo) { // FNV-1a 64-bit const ulong fnvOffsetBasis = 14695981039346656037UL; const ulong fnvPrime = 1099511628211UL; var hash = fnvOffsetBasis; foreach (var c in replyTo) { hash ^= (byte)c; hash *= fnvPrime; } // Return as non-negative long return (long)(hash & 0x7FFFFFFFFFFFFFFF); } /// /// Converts a reply subject to gateway form with an explicit hash segment. /// Format: _GR_.{clusterId}.{hash}.{originalReply}. /// public static string? ToGatewayReply(string? replyTo, string localClusterId, long hash) { if (string.IsNullOrWhiteSpace(replyTo)) return replyTo; return $"{GatewayReplyPrefix}{localClusterId}.{hash}.{replyTo}"; } /// /// Converts a reply subject to gateway form, automatically computing the hash. /// Format: _GR_.{clusterId}.{hash}.{originalReply}. /// public static string? ToGatewayReply(string? replyTo, string localClusterId) { if (string.IsNullOrWhiteSpace(replyTo)) return replyTo; var hash = ComputeReplyHash(replyTo); return ToGatewayReply(replyTo, localClusterId, hash); } /// /// Restores the original reply subject from a gateway-prefixed reply. /// Handles both new format (_GR_.{clusterId}.{hash}.{originalReply}) and /// legacy format (_GR_.{clusterId}.{originalReply}). /// Nested prefixes are unwrapped iteratively. /// public static bool TryRestoreGatewayReply(string? gatewayReply, out string restoredReply) { restoredReply = string.Empty; if (!HasGatewayReplyPrefix(gatewayReply)) return false; var current = gatewayReply!; while (HasGatewayReplyPrefix(current)) { // Skip the "_GR_." prefix var afterPrefix = current[GatewayReplyPrefix.Length..]; // Find the first dot (end of clusterId) var firstDot = afterPrefix.IndexOf('.'); if (firstDot < 0 || firstDot == afterPrefix.Length - 1) return false; var afterCluster = afterPrefix[(firstDot + 1)..]; // Check if the next segment is a numeric hash var secondDot = afterCluster.IndexOf('.'); if (secondDot > 0 && secondDot < afterCluster.Length - 1 && IsNumericSegment(afterCluster.AsSpan()[..secondDot])) { // New format: skip hash segment too current = afterCluster[(secondDot + 1)..]; } else { // Legacy format: no hash, the rest is the original reply current = afterCluster; } } restoredReply = current; return true; } /// /// Extracts the cluster ID from a gateway reply subject. /// The cluster ID is the first segment after the _GR_. prefix. /// public static bool TryExtractClusterId(string? gatewayReply, out string clusterId) { clusterId = string.Empty; if (!HasGatewayReplyPrefix(gatewayReply)) return false; var afterPrefix = gatewayReply![GatewayReplyPrefix.Length..]; var dot = afterPrefix.IndexOf('.'); if (dot <= 0) return false; clusterId = afterPrefix[..dot]; return true; } /// /// Extracts the hash from a gateway reply subject (new format only). /// Returns false if the reply uses the legacy format without a hash. /// public static bool TryExtractHash(string? gatewayReply, out long hash) { hash = 0; if (!HasGatewayReplyPrefix(gatewayReply)) return false; var afterPrefix = gatewayReply![GatewayReplyPrefix.Length..]; // Skip clusterId var firstDot = afterPrefix.IndexOf('.'); if (firstDot <= 0 || firstDot == afterPrefix.Length - 1) return false; var afterCluster = afterPrefix[(firstDot + 1)..]; // Try to parse hash segment var secondDot = afterCluster.IndexOf('.'); if (secondDot <= 0) return false; var hashSegment = afterCluster[..secondDot]; return long.TryParse(hashSegment, out hash); } private static bool IsNumericSegment(ReadOnlySpan segment) { if (segment.IsEmpty) return false; foreach (var c in segment) { if (c is not (>= '0' and <= '9')) return false; } return true; } }