using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NetSerializer;
using Newtonsoft.Json.Linq;
using Robust.Shared.ContentPack;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.Serialization
{
///
/// Serializer which manages a mapping of pre-loaded strings to constant
/// values, for message compression. The mapping is shared between the
/// server and client.
///
///
/// Strings are long and expensive to send over the wire, and lots of
/// strings involved in messages are sent repeatedly between the server
/// and client - such as filenames, icon states, constant strings, etc.
///
/// To compress these strings, we use a constant string mapping, decided
/// by the server when it starts up, that associates strings with a
/// fixed value. The mapping is shared with clients when they connect.
///
/// When sending these strings over the wire, the serializer can then
/// send the constant value instead - and at the other end, the
/// serializer can use the same mapping to recover the original string.
///
public class RobustMappedStringSerializer : IStaticTypeSerializer, IRobustMappedStringSerializer
{
private INetManager? _net;
private readonly Lazy _lazyLogSzr = new Lazy(() => Logger.GetSawmill("szr"));
private ISawmill LogSzr => _lazyLogSzr.Value;
private readonly HashSet _incompleteHandshakes = new HashSet();
///
/// Starts the handshake from the server end of the given channel,
/// sending a .
///
/// The network channel to perform the handshake over.
///
/// Locks the string mapping if this is the first time the server is
/// performing the handshake.
///
///
///
public async Task Handshake(INetChannel channel)
{
var net = channel.NetPeer;
if (net.IsClient)
{
return;
}
if (!LockMappedStrings)
{
LockMappedStrings = true;
LogSzr.Debug($"Locked in at {_mappedStrings.Count} mapped strings.");
}
_incompleteHandshakes.Add(channel);
var message = net.CreateNetMessage();
message.Hash = MappedStringsHash;
net.ServerSendMessage(message, channel);
while (_incompleteHandshakes.Contains(channel))
{
await Task.Delay(1);
}
LogSzr.Debug($"Completed handshake with {channel.RemoteEndPoint.Address}.");
}
///
/// Performs the setup so that the serializer can perform the string-
/// exchange protocol.
///
///
/// The string-exchange protocol is started by the server when the
/// client first connects. The server sends the client a hash of the
/// string mapping; the client checks that hash against any local
/// caches; and if necessary, the client requests a new copy of the
/// mapping from the server.
///
/// Uncached flow:
/// Client | Server
/// | <-------------- Hash |
/// | Need Strings ------> |
/// | <----------- Strings |
/// | Dont Need Strings -> |
///
///
/// Cached flow:
/// Client | Server
/// | <-------------- Hash |
/// | Dont Need Strings -> |
///
///
/// Verification failure flow:
/// Client | Server
/// | <-------------- Hash |
/// | Need Strings ------> |
/// | <----------- Strings |
/// + Hash Failed |
/// | Need Strings ------> |
/// | <----------- Strings |
/// | Dont Need Strings -> |
///
///
/// NOTE: Verification failure flow is currently not implemented.
///
///
/// The to perform the protocol steps over.
///
///
///
///
///
///
///
///
public void NetworkInitialize(INetManager net)
{
_net = net;
net.RegisterNetMessage(
nameof(MsgRobustMappedStringsSerializerServerHandshake),
msg => HandleServerHandshake(net, msg));
net.RegisterNetMessage(
nameof(MsgRobustMappedStringsSerializerClientHandshake),
msg => HandleClientHandshake(net, msg));
net.RegisterNetMessage(
nameof(MsgRobustMappedStringsSerializerStrings),
msg => HandleStringsMessage(net, msg));
}
///
/// Handles the reception, verification of a strings package
/// and subsequent mapping of strings and initiator of
/// receipt response.
///
/// Uncached flow:
/// Client | Server
/// | <-------------- Hash |
/// | Need Strings ------> |
/// | <----------- Strings |
/// | Dont Need Strings -> | <- you are here on client
///
/// Verification failure flow:
/// Client | Server
/// | <-------------- Hash |
/// | Need Strings ------> |
/// | <----------- Strings |
/// + Hash Failed | <- you are here on client
/// | Need Strings ------> |
/// | <----------- Strings |
/// | Dont Need Strings -> | <- you are here on client
///
///
/// NOTE: Verification failure flow is currently not implemented.
///
///
/// Unable to verify strings package by hash.
///
private void HandleStringsMessage(INetManager net, MsgRobustMappedStringsSerializerStrings msgRobustMappedStringsSerializer)
{
if (net.IsServer)
{
LogSzr.Error("Received strings from client.");
return;
}
LockMappedStrings = false;
ClearStrings();
DebugTools.Assert(msgRobustMappedStringsSerializer.Package != null, "msg.Package != null");
LoadStrings(new MemoryStream(msgRobustMappedStringsSerializer.Package!, false));
var checkHash = CalculateHash(msgRobustMappedStringsSerializer.Package!);
if (!checkHash.SequenceEqual(ServerHash))
{
// TODO: retry sending MsgClientHandshake with NeedsStrings = false
throw new InvalidOperationException("Unable to verify strings package by hash." + $"\n{ConvertToBase64Url(checkHash)} vs. {ConvertToBase64Url(ServerHash)}");
}
_stringMapHash = ServerHash;
LockMappedStrings = true;
LogSzr.Debug($"Locked in at {_mappedStrings.Count} mapped strings.");
WriteStringCache();
// ok we're good now
var channel = msgRobustMappedStringsSerializer.MsgChannel;
OnClientCompleteHandshake(net, channel);
}
///
/// Interpret a client's handshake, either sending a package
/// of strings or completing the handshake.
///
/// Uncached flow:
/// Client | Server
/// | <-------------- Hash |
/// | Need Strings ------> | <- you are here on server
/// | <----------- Strings |
/// | Dont Need Strings -> | <- you are here on server
///
///
/// Cached flow:
/// Client | Server
/// | <-------------- Hash |
/// | Dont Need Strings -> | <- you are here on server
///
///
/// Verification failure flow:
/// Client | Server
/// | <-------------- Hash |
/// | Need Strings ------> | <- you are here on server
/// | <----------- Strings |
/// + Hash Failed |
/// | Need Strings ------> | <- you are here on server
/// | <----------- Strings |
/// | Dont Need Strings -> |
///
///
/// NOTE: Verification failure flow is currently not implemented.
///
///
private void HandleClientHandshake(INetManager net, MsgRobustMappedStringsSerializerClientHandshake msgRobustMappedStringsSerializer)
{
if (net.IsClient)
{
LogSzr.Error("Received client handshake on client.");
return;
}
LogSzr.Debug($"Received handshake from {msgRobustMappedStringsSerializer.MsgChannel.RemoteEndPoint.Address}.");
if (!msgRobustMappedStringsSerializer.NeedsStrings)
{
LogSzr.Debug($"Completing handshake with {msgRobustMappedStringsSerializer.MsgChannel.RemoteEndPoint.Address}.");
_incompleteHandshakes.Remove(msgRobustMappedStringsSerializer.MsgChannel);
return;
}
// TODO: count and limit number of requests to send strings during handshake
var strings = msgRobustMappedStringsSerializer.MsgChannel.NetPeer.CreateNetMessage();
using (var ms = new MemoryStream())
{
WriteStringPackage(ms);
ms.Position = 0;
strings.Package = ms.ToArray();
LogSzr.Debug($"Sending {ms.Length} bytes sized mapped strings package to {msgRobustMappedStringsSerializer.MsgChannel.RemoteEndPoint.Address}.");
}
msgRobustMappedStringsSerializer.MsgChannel.SendMessage(strings);
}
///
/// Interpret a server's handshake, either requesting a package
/// of strings or completing the handshake.
///
/// Uncached flow:
/// Client | Server
/// | <-------------- Hash | <- you are here on client
/// | Need Strings ------> |
/// | <----------- Strings |
/// | Dont Need Strings -> |
///
///
/// Cached flow:
/// Client | Server
/// | <-------------- Hash | <- you are here on client
/// | Dont Need Strings -> |
///
///
/// Verification failure flow:
/// Client | Server
/// | <-------------- Hash | <- you are here on client
/// | Need Strings ------> |
/// | <----------- Strings |
/// + Hash Failed |
/// | Need Strings ------> |
/// | <----------- Strings |
/// | Dont Need Strings -> |
///
///
/// NOTE: Verification failure flow is currently not implemented.
///
/// Mapped strings are locked.
///
private void HandleServerHandshake(INetManager net, MsgRobustMappedStringsSerializerServerHandshake msgRobustMappedStringsSerializer)
{
if (net.IsServer)
{
LogSzr.Error("Received server handshake on server.");
return;
}
ServerHash = msgRobustMappedStringsSerializer.Hash;
LockMappedStrings = false;
if (LockMappedStrings)
{
throw new InvalidOperationException("Mapped strings are locked.");
}
ClearStrings();
var hashStr = ConvertToBase64Url(Convert.ToBase64String(msgRobustMappedStringsSerializer.Hash!));
LogSzr.Debug($"Received server handshake with hash {hashStr}.");
var fileName = CacheForHash(hashStr);
if (!File.Exists(fileName))
{
LogSzr.Debug($"No string cache for {hashStr}.");
var handshake = net.CreateNetMessage();
LogSzr.Debug("Asking server to send mapped strings.");
handshake.NeedsStrings = true;
msgRobustMappedStringsSerializer.MsgChannel.SendMessage(handshake);
}
else
{
LogSzr.Debug($"We had a cached string map that matches {hashStr}.");
using var file = File.OpenRead(fileName);
var added = LoadStrings(file);
_stringMapHash = msgRobustMappedStringsSerializer.Hash!;
LogSzr.Debug($"Read {added} strings from cache {hashStr}.");
LockMappedStrings = true;
LogSzr.Debug($"Locked in at {_mappedStrings.Count} mapped strings.");
// ok we're good now
var channel = msgRobustMappedStringsSerializer.MsgChannel;
OnClientCompleteHandshake(net, channel);
}
}
///
/// Inform the server that the client has a complete copy of the
/// mapping, and alert other code that the handshake is over.
///
///
///
private void OnClientCompleteHandshake(INetManager net, INetChannel channel)
{
LogSzr.Debug("Letting server know we're good to go.");
var handshake = net.CreateNetMessage();
handshake.NeedsStrings = false;
channel.SendMessage(handshake);
if (ClientHandshakeComplete == null)
{
LogSzr.Warning("There's no handler attached to ClientHandshakeComplete.");
}
ClientHandshakeComplete?.Invoke();
}
///
/// Gets the cache file associated with the given hash.
///
/// The hash to look up the cache for.
///
/// The filename where the cache for the given hash would be. The
/// file itself may or may not exist. If it does not exist, no cache
/// was made for the given hash.
///
private string CacheForHash(string hashStr)
=> PathHelpers.ExecutableRelativeFile($"strings-{hashStr}");
///
/// Saves the string cache to a file based on it's hash.
///
private void WriteStringCache()
{
var hashStr = Convert.ToBase64String(MappedStringsHash);
hashStr = ConvertToBase64Url(hashStr);
var fileName = CacheForHash(hashStr);
using var file = File.OpenWrite(fileName);
WriteStringPackage(file);
LogSzr.Debug($"Wrote string cache {hashStr}.");
}
private byte[]? _mappedStringsPackage;
private byte[] MappedStringsPackage => LockMappedStrings
? _mappedStringsPackage ??= WriteStringPackage()
: throw new InvalidOperationException("Mapped strings must be locked.");
///
/// Writes strings to a package and converts to an array of bytes.
///
///
/// This is invoked by accessing for the first time.
///
private byte[] WriteStringPackage()
{
using var ms = new MemoryStream();
WriteStringPackage(ms);
return ms.ToArray();
}
///
/// Writes a strings package to a stream.
///
/// A writable stream.
/// Overly long string in strings package.
public void WriteStringPackage(Stream stream)
{
// ReSharper disable once SuggestVarOrType_Elsewhere
Span buf = stackalloc byte[MaxMappedStringSize];
var sw = Stopwatch.StartNew();
var enc = Encoding.UTF8.GetEncoder();
using (var zs = new DeflateStream(stream, CompressionLevel.Optimal, true))
{
var bytesWritten = WriteCompressedUnsignedInt(zs, (uint) MappedStrings.Count);
foreach (var str in MappedStrings)
{
if (str.Length >= MaxMappedStringSize)
{
throw new NotImplementedException("Overly long string in strings package.");
}
var l = enc.GetBytes(str, buf, true);
if (l >= MaxMappedStringSize)
{
throw new NotImplementedException("Overly long string in strings package.");
}
bytesWritten += WriteCompressedUnsignedInt(zs, (uint) l);
zs.Write(buf.Slice(0,l));
bytesWritten += l;
enc.Reset();
}
zs.Write(BitConverter.GetBytes(bytesWritten));
zs.Flush();
}
LogSzr.Debug($"Wrote {MappedStrings.Count} strings to package in {sw.ElapsedMilliseconds}ms.");
}
///
/// Loads a strings package from a stream.
///
///
/// Uses to extract strings and adds them to the mapping.
///
/// A readable stream.
/// The number of strings loaded.
/// Mapped strings are locked, will not load.
/// Did not read all bytes in package!
private int LoadStrings(Stream stream)
{
if (LockMappedStrings)
{
throw new InvalidOperationException("Mapped strings are locked, will not load.");
}
var started = MappedStrings.Count;
foreach (var str in ReadStringPackage(stream))
{
_stringMapping[str] = _mappedStrings.Count;
_mappedStrings.Add(str);
}
if (stream.CanSeek && stream.CanRead)
{
if (stream.Position != stream.Length)
{
throw new InvalidDataException("Did not read all bytes in package!");
}
}
var added = MappedStrings.Count - started;
return added;
}
///
/// Reads the contents of a strings package.
///
///
/// Does not add strings to the current mapping.
///
/// A readable stream.
/// Strings from within the package.
/// Could not read the full length of string #N.
private IEnumerable ReadStringPackage(Stream stream)
{
var buf = ArrayPool.Shared.Rent(65536);
var sw = Stopwatch.StartNew();
using var zs = new DeflateStream(stream, CompressionMode.Decompress);
var c = ReadCompressedUnsignedInt(zs, out var x);
var bytesRead = x;
for (var i = 0; i < c; ++i)
{
var l = (int) ReadCompressedUnsignedInt(zs, out x);
bytesRead += x;
var y = zs.Read(buf, 0, l);
if (y != l)
{
throw new InvalidDataException($"Could not read the full length of string #{i}.");
}
bytesRead += y;
var str = Encoding.UTF8.GetString(buf, 0, l);
yield return str;
}
zs.Read(buf, 0, 4);
var checkBytesRead = BitConverter.ToInt32(buf, 0);
if (checkBytesRead != bytesRead)
{
throw new InvalidDataException("Could not verify package was read correctly.");
}
LogSzr.Debug($"Read package of {c} strings in {sw.ElapsedMilliseconds}ms.");
}
///
/// Converts a byte array such as a hash to a Base64 representation that is URL safe.
///
///
/// A base64url string form of the byte array.
private string ConvertToBase64Url(byte[]? data)
=> data == null ? "" : ConvertToBase64Url(Convert.ToBase64String(data));
///
/// Converts a a Base64 string to one that is URL safe.
///
/// A base64url formed string.
private string ConvertToBase64Url(string b64Str)
{
if (b64Str is null)
{
throw new ArgumentNullException(nameof(b64Str));
}
var cut = b64Str[^1] == '=' ? b64Str[^2] == '=' ? 2 : 1 : 0;
b64Str = new StringBuilder(b64Str).Replace('+', '-').Replace('/', '_').ToString(0, b64Str.Length - cut);
return b64Str;
}
///
/// Converts a URL-safe Base64 string into a byte array.
///
/// A base64url formed string.
/// The represented byte array.
public byte[] ConvertFromBase64Url(string s)
{
var l = s.Length % 3;
var sb = new StringBuilder(s);
sb.Replace('-', '+').Replace('_', '/');
for (var i = 0; i < l; ++i)
{
sb.Append('=');
}
s = sb.ToString();
return Convert.FromBase64String(s);
}
public byte[]? ServerHash;
private readonly List _mappedStrings = new List();
private readonly Dictionary _stringMapping = new Dictionary();
public IReadOnlyList MappedStrings => new ReadOnlyCollection(_mappedStrings);
///
/// Whether the string mapping is decided, and cannot be changed.
///
///
///
/// While false, strings can be added to the mapping, but
/// it cannot be saved to a cache.
///
///
/// While true, the mapping cannot be modified, but can be
/// shared between the server and client and saved to a cache.
///
///
public bool LockMappedStrings { get; set; }
private readonly Regex _rxSymbolSplitter
= new Regex(
@"(?<=[^\s\W])(?=[A-Z]) # Match for split at start of new capital letter
|(?<=[^0-9\s\W])(?=[0-9]) # Match for split before spans of numbers
|(?<=[A-Za-z0-9])(?=_) # Match for a split before an underscore
|(?=[.\\\/,#$?!@|&*()^`""'`~[\]{}:;\-]) # Match for a split after symbols
|(?<=[.\\\/,#$?!@|&*()^`""'`~[\]{}:;\-]) # Match for a split before symbols too",
RegexOptions.CultureInvariant
| RegexOptions.Compiled
| RegexOptions.IgnorePatternWhitespace
);
///
/// Add a string to the constant mapping.
///
///
/// If the string has multiple detectable subcomponents, such as a
/// filepath, it may result in more than one string being added to
/// the mapping. As string parts are commonly sent as subsets or
/// scoped names, this increases the likelyhood of a successful
/// string mapping.
///
///
/// true if the string was added to the mapping for the first
/// time, false otherwise.
///
///
/// Thrown if the string is not normalized ().
///
public bool AddString(string str)
{
if (LockMappedStrings)
{
if (_net!.IsClient)
{
//LogSzr.Debug("On client and mapped strings are locked, will not add.");
return false;
}
//throw new InvalidOperationException("Mapped strings are locked, will not add.");
LogSzr.Debug("On server and mapped strings are locked, will not add.");
return false;
}
if (String.IsNullOrEmpty(str))
{
return false;
}
if (!str.IsNormalized())
{
throw new InvalidOperationException("Only normalized strings may be added.");
}
if (_stringMapping.ContainsKey(str))
{
return false;
}
if (str.Length >= MaxMappedStringSize) return false;
if (str.Length <= MinMappedStringSize) return false;
str = str.Trim();
if (str.Length <= MinMappedStringSize) return false;
str = str.Replace(Environment.NewLine, "\n");
if (str.Length <= MinMappedStringSize) return false;
var symTrimmedStr = str.Trim(TrimmableSymbolChars);
if (symTrimmedStr != str)
{
AddString(symTrimmedStr);
}
if (str.Contains('/'))
{
var parts = str.Split('/', StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < parts.Length; ++i)
{
for (var l = 1; l <= parts.Length - i; ++l)
{
var subStr = String.Join('/', parts.Skip(i).Take(l));
if (_stringMapping.TryAdd(subStr, _mappedStrings.Count))
{
_mappedStrings.Add(subStr);
}
if (!subStr.Contains('.'))
{
continue;
}
var subParts = subStr.Split('.', StringSplitOptions.RemoveEmptyEntries);
for (var si = 0; si < subParts.Length; ++si)
{
for (var sl = 1; sl <= subParts.Length - si; ++sl)
{
var subSubStr = String.Join('.', subParts.Skip(si).Take(sl));
// ReSharper disable once InvertIf
if (_stringMapping.TryAdd(subSubStr, _mappedStrings.Count))
{
_mappedStrings.Add(subSubStr);
}
}
}
}
}
}
else if (str.Contains("_"))
{
foreach (var substr in str.Split("_"))
{
AddString(substr);
}
}
else if (str.Contains(" "))
{
foreach (var substr in str.Split(" "))
{
if (substr == str) continue;
AddString(substr);
}
}
else
{
var parts = _rxSymbolSplitter.Split(str);
foreach (var substr in parts)
{
if (substr == str) continue;
AddString(substr);
}
for (var si = 0; si < parts.Length; ++si)
{
for (var sl = 1; sl <= parts.Length - si; ++sl)
{
var subSubStr = String.Concat(parts.Skip(si).Take(sl));
if (_stringMapping.TryAdd(subSubStr, _mappedStrings.Count))
{
_mappedStrings.Add(subSubStr);
}
}
}
}
if (_stringMapping.TryAdd(str, _mappedStrings.Count))
{
_mappedStrings.Add(str);
}
_stringMapHash = null;
_mappedStringsPackage = null;
return true;
}
///
/// Add the constant strings from an to the
/// mapping.
///
/// The assembly from which to collect constant strings.
[MethodImpl(MethodImplOptions.Synchronized)]
public unsafe void AddStrings(Assembly asm)
{
if (LockMappedStrings)
{
if (_net!.IsClient)
{
//LogSzr.Debug("On client and mapped strings are locked, will not add.");
return;
}
//throw new InvalidOperationException("Mapped strings are locked, will not add .");
LogSzr.Debug("On server and mapped strings are locked, will not add.");
return;
}
var started = MappedStrings.Count;
var sw = Stopwatch.StartNew();
if (asm.TryGetRawMetadata(out var blob, out var len))
{
var reader = new MetadataReader(blob, len);
var usrStrHandle = default(UserStringHandle);
do
{
var userStr = reader.GetUserString(usrStrHandle);
if (userStr != "")
{
AddString(String.Intern(userStr.Normalize()));
}
usrStrHandle = reader.GetNextHandle(usrStrHandle);
} while (usrStrHandle != default);
var strHandle = default(StringHandle);
do
{
var str = reader.GetString(strHandle);
if (str != "")
{
AddString(String.Intern(str.Normalize()));
}
strHandle = reader.GetNextHandle(strHandle);
} while (strHandle != default);
}
var added = MappedStrings.Count - started;
LogSzr.Debug($"Mapping {added} strings from {asm.GetName().Name} took {sw.ElapsedMilliseconds}ms.");
}
///
/// Add strings from the given to the mapping.
///
///
/// Strings are taken from YAML anchors, tags, and leaf nodes.
///
/// The YAML to collect strings from.
/// The stream name. Only used for logging.
[MethodImpl(MethodImplOptions.Synchronized)]
public void AddStrings(YamlStream yaml, string name)
{
if (LockMappedStrings)
{
if (_net!.IsClient)
{
//LogSzr.Debug("On client and mapped strings are locked, will not add.");
return;
}
//throw new InvalidOperationException("Mapped strings are locked, will not add.");
LogSzr.Debug("On server and mapped strings are locked, will not add.");
return;
}
var started = MappedStrings.Count;
var sw = Stopwatch.StartNew();
foreach (var doc in yaml)
{
foreach (var node in doc.AllNodes)
{
var a = node.Anchor;
if (!String.IsNullOrEmpty(a))
{
AddString(a);
}
var t = node.Tag;
if (!String.IsNullOrEmpty(t))
{
AddString(t);
}
switch (node)
{
case YamlScalarNode scalar:
{
var v = scalar.Value;
if (String.IsNullOrEmpty(v))
{
continue;
}
AddString(v);
break;
}
}
}
}
var added = MappedStrings.Count - started;
LogSzr.Debug($"Mapping {added} strings from {name} took {sw.ElapsedMilliseconds}ms.");
}
///
/// Add strings from the given to the mapping.
///
///
/// Strings are taken from JSON property names and string nodes.
///
/// The JSON to collect strings from.
/// The stream name. Only used for logging.
public void AddStrings(JObject obj, string name)
{
if (LockMappedStrings)
{
if (_net!.IsClient)
{
//LogSzr.Debug("On client and mapped strings are locked, will not add.");
return;
}
//throw new InvalidOperationException("Mapped strings are locked, will not add.");
LogSzr.Debug("On server and mapped strings are locked, will not add.");
return;
}
var started = MappedStrings.Count;
var sw = Stopwatch.StartNew();
foreach (var node in obj.DescendantsAndSelf())
{
switch (node)
{
case JValue value:
{
if (value.Type != JTokenType.String)
{
continue;
}
var v = value.Value?.ToString();
if (String.IsNullOrEmpty(v))
{
continue;
}
AddString(v);
break;
}
case JProperty prop:
{
var propName = prop.Name;
if (String.IsNullOrEmpty(propName))
{
continue;
}
AddString(propName);
break;
}
}
}
var added = MappedStrings.Count - started;
LogSzr.Debug($"Mapping {added} strings from {name} took {sw.ElapsedMilliseconds}ms.");
}
///
/// Remove all strings from the mapping, completely resetting it.
///
///
/// Thrown if the mapping is locked.
///
public void ClearStrings()
{
if (LockMappedStrings)
{
throw new InvalidOperationException("Mapped strings are locked, will not clear.");
}
_mappedStrings.Clear();
_stringMapping.Clear();
_stringMapHash = null;
}
///
/// Add strings from the given enumeration to the mapping.
///
/// The strings to add.
/// The source provider of the strings to be logged.
[MethodImpl(MethodImplOptions.Synchronized)]
public void AddStrings(IEnumerable strings, string providerName)
{
if (LockMappedStrings)
{
if (_net!.IsClient)
{
//LogSzr.Debug("On client and mapped strings are locked, will not add.");
return;
}
//throw new InvalidOperationException("Mapped strings are locked, will not add.");
LogSzr.Debug("On server and mapped strings are locked, will not add.");
return;
}
var started = MappedStrings.Count;
foreach (var str in strings)
{
AddString(str);
}
var added = MappedStrings.Count - started;
LogSzr.Debug($"Mapping {added} strings from {providerName}.");
}
private byte[]? _stringMapHash;
///
/// The hash of the string mapping.
///
///
/// Thrown if the mapping is not locked.
///
public byte[] MappedStringsHash => _stringMapHash ??= CalculateMappedStringsHash();
private byte[] CalculateMappedStringsHash()
{
if (!LockMappedStrings)
{
throw new InvalidOperationException("String table should be locked before attempting to retrieve hash.");
}
var sw = Stopwatch.StartNew();
var hash = CalculateHash(MappedStringsPackage);
LogSzr.Debug($"Hashing {MappedStrings.Count} strings took {sw.ElapsedMilliseconds}ms.");
LogSzr.Debug($"Size: {MappedStringsPackage.Length} bytes, Hash: {ConvertToBase64Url(hash)}");
return hash;
}
///
/// Creates a SHA512 hash of the given array of bytes.
///
/// An array of bytes to be hashed.
/// A 512-bit (64-byte) hash result as an array of bytes.
///
private byte[] CalculateHash(byte[] data)
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
using var hasher = SHA512.Create();
var hash = hasher.ComputeHash(data);
return hash;
}
///
/// Implements .
/// Specifies that this implementation handles strings.
///
public bool Handles(Type type) => type == typeof(string);
///
/// Implements .
///
public IEnumerable GetSubtypes(Type type) => Type.EmptyTypes;
///
/// Implements .
///
///
public MethodInfo GetStaticWriter(Type type) => WriteMappedStringMethodInfo;
///
/// Implements .
///
///
public MethodInfo GetStaticReader(Type type) => ReadMappedStringMethodInfo;
private delegate void WriteStringDelegate(Stream stream, string? value);
private delegate void ReadStringDelegate(Stream stream, out string? value);
private static readonly MethodInfo WriteMappedStringMethodInfo
= ((WriteStringDelegate) StaticWriteMappedString).Method;
private static readonly MethodInfo ReadMappedStringMethodInfo
= ((ReadStringDelegate) StaticReadMappedString).Method;
private static readonly char[] TrimmableSymbolChars =
{
'.', '\\', '/', ',', '#', '$', '?', '!', '@', '|', '&',
'*', '(', ')', '^', '`', '"', '\'', '`', '~', '[', ']',
'{', '}', ':', ';', '-'
};
///
/// The shortest a string can be in order to be inserted in the mapping.
///
///
/// Strings below a certain length aren't worth compressing.
///
private const int MinMappedStringSize = 3;
///
/// The longest a string can be in order to be inserted in the mapping.
///
private const int MaxMappedStringSize = 420;
///
/// The special value corresponding to a null string in the
/// encoding.
///
private const int MappedNull = 0;
///
/// The special value corresponding to a string which was not mapped.
/// This is followed by the bytes of the unmapped string.
///
private const int UnmappedString = 1;
///
/// The first non-special value, used for encoding mapped strings.
///
///
/// Since previous values are taken by and
/// , this value is used to encode
/// mapped strings at an offset - in the encoding, a value
/// >= FirstMappedIndexStart represents the string with
/// mapping of that value - FirstMappedIndexStart.
///
private const int FirstMappedIndexStart = 2;
///
/// Write the encoding of the given string to the stream.
/// Static form of for use by .
///
/// The stream to write to.
/// The (possibly null) string to write.
public static void StaticWriteMappedString(Stream stream, string? value)
{
var mss = IoCManager.Resolve();
mss.WriteMappedString(stream, value);
}
///
/// Write the encoding of the given string to the stream.
///
/// The stream to write to.
/// The (possibly null) string to write.
public void WriteMappedString(Stream stream, string? value)
{
if (!LockMappedStrings)
{
LogSzr.Warning("Performing unlocked string mapping.");
}
if (value == null)
{
WriteCompressedUnsignedInt(stream, MappedNull);
return;
}
if (_stringMapping.TryGetValue(value, out var mapping))
{
#if DEBUG
if (mapping >= _mappedStrings.Count || mapping < 0)
{
throw new InvalidOperationException("A string mapping outside of the mapped string table was encountered.");
}
#endif
WriteCompressedUnsignedInt(stream, (uint) mapping + FirstMappedIndexStart);
//Logger.DebugS("szr", $"Encoded mapped string: {value}");
return;
}
// indicate not mapped
WriteCompressedUnsignedInt(stream, UnmappedString);
var buf = Encoding.UTF8.GetBytes(value);
//Logger.DebugS("szr", $"Encoded unmapped string: {value}");
WriteCompressedUnsignedInt(stream, (uint) buf.Length);
stream.Write(buf);
}
///
/// Try to read a string from the given stream.
/// Static form of for use by .
///
/// The stream to read from.
/// The (possibly null) string read.
///
/// Thrown if the mapping is not locked.
///
public static void StaticReadMappedString(Stream stream, out string? value)
{
var mss = IoCManager.Resolve();
mss.ReadMappedString(stream, out value);
}
///
/// Try to read a string from the given stream.
///
/// The stream to read from.
/// The (possibly null) string read.
///
/// Thrown if the mapping is not locked.
///
public void ReadMappedString(Stream stream, out string? value)
{
if (!LockMappedStrings)
{
throw new InvalidOperationException("Not performing unlocked string mapping.");
}
var mapIndex = ReadCompressedUnsignedInt(stream, out _);
if (mapIndex == MappedNull)
{
value = null;
return;
}
if (mapIndex == UnmappedString)
{
// not mapped
var length = checked((int)ReadCompressedUnsignedInt(stream, out _));
// ReSharper disable once SuggestVarOrType_Elsewhere
Span buf = stackalloc byte[length];
stream.Read(buf);
value = Encoding.UTF8.GetString(buf);
//Logger.DebugS("szr", $"Decoded unmapped string: {value}");
return;
}
value = _mappedStrings[(int) mapIndex - FirstMappedIndexStart];
//Logger.DebugS("szr", $"Decoded mapped string: {value}");
}
// TODO: move the below methods to some stream helpers class
#if ROBUST_SERIALIZER_DISABLE_COMPRESSED_UINTS
public static int WriteCompressedUnsignedInt(Stream stream, uint value)
{
WriteUnsignedInt(stream, value);
return 4;
}
public static uint ReadCompressedUnsignedInt(Stream stream, out int byteCount)
{
byteCount = 4;
return ReadUnsignedInt(stream);
}
#else
public static int WriteCompressedUnsignedInt(Stream stream, uint value)
{
var length = 1;
while (value >= 0x80)
{
stream.WriteByte((byte) (0x80 | value));
value >>= 7;
++length;
}
stream.WriteByte((byte) value);
return length;
}
public static uint ReadCompressedUnsignedInt(Stream stream, out int byteCount)
{
byteCount = 0;
var value = 0u;
var shift = 0;
while (stream.CanRead)
{
var current = stream.ReadByte();
++byteCount;
if (current == -1)
{
throw new EndOfStreamException();
}
value |= (0x7Fu & (byte) current) << shift;
shift += 7;
if ((0x80 & current) == 0)
{
return value;
}
}
throw new EndOfStreamException();
}
#endif
[UsedImplicitly]
public static unsafe void WriteUnsignedInt(Stream stream, uint value)
{
var bytes = MemoryMarshal.AsBytes(new ReadOnlySpan(&value, 1));
stream.Write(bytes);
}
[UsedImplicitly]
public static unsafe uint ReadUnsignedInt(Stream stream)
{
uint value;
var bytes = MemoryMarshal.AsBytes(new Span(&value, 1));
stream.Read(bytes);
return value;
}
///
/// See .
///
public event Action? ClientHandshakeComplete;
}
}