Files
Rendezvous/src/FinalFactory.Rendezvous.Contracts/ContractValidation.cs
T
KyuubiYoru 94aba8a3bb
quality-gate / quality (push) Successful in 59s
feat(client): standardize connection outcomes (#13)
2026-07-16 10:18:41 +02:00

154 lines
5.9 KiB
C#

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
namespace FinalFactory.Rendezvous.Contracts;
public static class ContractValidation
{
private const string CapabilityAlphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
public static RendezvousErrorCode ValidateContractVersion(int contractVersion) =>
contractVersion == ContractLimits.ContractVersion
? RendezvousErrorCode.None
: RendezvousErrorCode.UnsupportedContractVersion;
public static bool AreProtocolsCompatible(uint requested, uint offered) => requested == offered;
public static bool IsHttpRequestSizeValid(int byteCount) =>
byteCount is >= 0 and <= ContractLimits.HttpRequestMaxBytes;
public static bool IsBrowserResponseSizeValid(int byteCount) =>
byteCount is >= 0 and <= ContractLimits.BrowserResponseMaxBytes;
public static bool IsUtf8LengthWithin(string? value, int maximumBytes)
{
if (maximumBytes < 0)
{
throw new ArgumentOutOfRangeException(nameof(maximumBytes));
}
return value is not null && Encoding.UTF8.GetByteCount(value) <= maximumBytes;
}
public static bool IsPageSizeValid(int pageSize) =>
pageSize is >= 1 and <= ContractLimits.BrowserPageMaxItems;
public static bool IsIdempotencyKeyValid(string? value) =>
IsVisibleAsciiWithin(value, ContractLimits.IdempotencyKeyMaxCharacters);
public static bool IsCursorValid(string? value) =>
value is null || IsVisibleAsciiWithin(value, ContractLimits.CursorMaxCharacters);
public static bool IsDiagnosticCodeValid(string? value) =>
value is null || IsVisibleAsciiWithin(value, ContractLimits.DiagnosticCodeMaxCharacters);
public static bool IsReportableConnectionOutcome(ConnectionOutcomeKind outcome) => outcome is
ConnectionOutcomeKind.Connected
or ConnectionOutcomeKind.Cancelled
or ConnectionOutcomeKind.TimedOut
or ConnectionOutcomeKind.StaleHost
or ConnectionOutcomeKind.TransportFailed
or ConnectionOutcomeKind.FallbackOffered
or ConnectionOutcomeKind.AttemptExpired
or ConnectionOutcomeKind.NoHostPresence
or ConnectionOutcomeKind.MediatorUnavailable
or ConnectionOutcomeKind.PunchTimedOut
or ConnectionOutcomeKind.DirectConnectTimedOut
or ConnectionOutcomeKind.HostRejected
or ConnectionOutcomeKind.TransportError
or ConnectionOutcomeKind.ManagerStopped
or ConnectionOutcomeKind.Disposed;
public static bool IsBuildVersionValid(string? value) =>
!string.IsNullOrWhiteSpace(value)
&& IsUtf8LengthWithin(value, ContractLimits.BuildVersionMaxBytes);
public static bool IsDisplayNameValid(string? value) =>
!string.IsNullOrWhiteSpace(value)
&& IsUtf8LengthWithin(value, ContractLimits.DisplayNameMaxBytes);
public static bool IsOpaqueHttpCredentialValid(string? value) =>
value is not null
&& value.Length is > 0 and <= ContractLimits.OpaqueHttpCredentialMaxCharacters;
public static bool IsCapacityValid(SessionCapacity? capacity) =>
capacity is not null
&& capacity.MaximumPlayers is >= 1 and <= ContractLimits.SessionCapacityMaxPlayers
&& capacity.CurrentPlayers >= 0
&& capacity.CurrentPlayers <= capacity.MaximumPlayers;
public static bool IsCapabilityValid(string? capability) =>
IsBase64UrlValueValid(capability, ContractLimits.UdpCapabilityMaxCharacters);
public static bool IsConnectionTicketValid(string? ticket) =>
IsBase64UrlValueValid(ticket, ContractLimits.ConnectionTicketMaxCharacters);
public static bool IsNetworkEndpointValid(NetworkEndpoint? endpoint)
{
if (endpoint is null
|| endpoint.Port is < 1 or > ushort.MaxValue
|| !IPAddress.TryParse(endpoint.Address, out IPAddress? address))
{
return false;
}
return endpoint.AddressFamily switch
{
AddressFamilyKind.Ipv4 => address.AddressFamily == AddressFamily.InterNetwork,
AddressFamilyKind.Ipv6 => address.AddressFamily == AddressFamily.InterNetworkV6,
_ => false,
};
}
public static bool IsMetadataValid(IReadOnlyDictionary<string, string>? metadata)
{
if (metadata is null || metadata.Count > ContractLimits.MetadataMaxKeys)
{
return false;
}
foreach (KeyValuePair<string, string> item in metadata)
{
if (string.IsNullOrWhiteSpace(item.Key)
|| !IsUtf8LengthWithin(item.Key, ContractLimits.MetadataKeyMaxBytes)
|| !IsUtf8LengthWithin(item.Value, ContractLimits.MetadataValueMaxBytes))
{
return false;
}
}
byte[] encoded = JsonSerializer.SerializeToUtf8Bytes(metadata, ContractJson.Options);
return encoded.Length <= ContractLimits.MetadataMaxBytes;
}
internal static bool IsSlug(string? value, int maximumCharacters)
{
if (string.IsNullOrEmpty(value)
|| value.Length > maximumCharacters
|| value[0] is < 'a' or > 'z')
{
return false;
}
return value.All(static character =>
character is >= 'a' and <= 'z'
or >= '0' and <= '9'
or '-');
}
private static bool IsBase64UrlValueValid(string? value, int maximumCharacters) =>
value is not null
&& value.Length is > 0
&& value.Length <= maximumCharacters
&& value.All(static character => CapabilityAlphabet.Contains(character));
private static bool IsVisibleAsciiWithin(string? value, int maximumCharacters) =>
value is not null
&& value.Length is > 0
&& value.Length <= maximumCharacters
&& value.All(static character => character is >= '!' and <= '~');
}