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;
}
}