using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NetSerializer;
using Prometheus;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
using static Robust.Shared.Utility.Base64Helpers;
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.
///
internal sealed partial class RobustMappedStringSerializer : IDynamicTypeSerializer, IRobustMappedStringSerializer
{
private static readonly Counter StringsHitMetric = Metrics.CreateCounter(
"robust_net_string_hit",
"Amount of strings sent that hit the mapped string dictionary.");
private static readonly Counter StringsMissMetric = Metrics.CreateCounter(
"robust_net_string_miss",
"Amount of strings sent that missed the mapped string dictionary.");
private static readonly Counter StringsMissCharsMetric = Metrics.CreateCounter(
"robust_net_string_miss_chars",
"Amount of extra chars (UTF-16, not bytes!!!) that have to be sent due to mapped string misses.");
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 uint 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 uint 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 uint FirstMappedIndexStart = 2;
[Dependency] private INetManager _net = default!;
// I don't want to create 50 line changes in this commit so...
// ReSharper disable once InconsistentNaming
private ISawmill LogSzr = default!;
private MappedStringDict _dict = default!;
private readonly Dictionary _incompleteHandshakes
= new();
private byte[]? _mappedStringsPackage;
private byte[]? _serverHash;
private byte[]? _stringMapHash;
///
/// The hash of the string mapping.
///
///
/// Thrown if the mapping is not locked.
///
public ReadOnlySpan MappedStringsHash => _stringMapHash;
public (byte[] mapHash, byte[] package) GeneratePackage() => _dict.GeneratePackage();
public void SetPackage(byte[] hash, byte[] package)
{
_dict.LoadFromPackage(package, out var hashResult);
if (!hashResult.SequenceEqual(hash!))
{
throw new InvalidOperationException("Hash mismatch when setting string package." +
$"\n{ConvertToBase64Url(hashResult)} vs. {ConvertToBase64Url(hash)}");
}
}
public bool EnableCaching { get; set; } = true;
private static readonly Regex RxSymbolSplitter
= new(
@"(?<=[^\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
);
public bool Locked => _dict.Locked;
public ITypeSerializer TypeSerializer => this;
///
/// 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 Task Handshake(INetChannel channel)
{
DebugTools.Assert(_net.IsServer);
DebugTools.Assert(_dict.Locked);
var tcs = new TaskCompletionSource