using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
///
/// Pure string surgery for two responses the client cannot cope with as the
/// server sends them. No engine or BestHTTP types live here so the rules can be
/// exercised outside the game.
///
internal static class ResponseRepair
{
/// Loopback probe target: any transport fails (or answers) instantly, so a probe phase can never stall.
internal const string LoopbackProbeAddress = "127.0.0.1:50000";
private const string QosPathSuffix = "/matchmaking/player/qos";
private const string UserSegmentPathSuffix = "/data/commonquery/usersegment";
private const string AddressKey = "\"address\"";
///
/// BestHTTP's chunked reader throws exactly this when the chunk-size line is
/// empty, which is what a keep-alive connection the server had already closed
/// looks like. It is not a bad request, so it must not fail one.
///
internal static bool IsTruncatedChunkFailure(string message)
{
if (string.IsNullOrEmpty(message)) return false;
return message.IndexOf("as a hex number", StringComparison.OrdinalIgnoreCase) >= 0;
}
///
/// Rewrites a response body when the client would otherwise mis-handle it.
/// Returns null when the body is left alone; explains
/// what changed when it did.
///
internal static string RepairBody(string path, string query, string body, out string note)
{
note = null;
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(body)) return null;
if (EndsWith(path, QosPathSuffix)) return RepairProbeList(body, ref note);
if (EndsWith(path, UserSegmentPathSuffix)) return RepairUserSegment(query, body, ref note);
return null;
}
///
/// The probe list points at retired GCP addresses: five of its six entries are
/// unreachable, so the client's region-probe phase waits out a ping timeout on
/// each boot and login sits there. Pinning every entry at loopback keeps the
/// list's shape but makes the probes answer (or refuse) immediately.
///
private static string RepairProbeList(string body, ref string note)
{
var repaired = new StringBuilder(body.Length);
var index = 0;
var replaced = 0;
while (true)
{
var key = body.IndexOf(AddressKey, index, StringComparison.Ordinal);
if (key < 0)
{
repaired.Append(body, index, body.Length - index);
break;
}
repaired.Append(body, index, key - index + AddressKey.Length);
var cursor = key + AddressKey.Length;
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++;
if (cursor >= body.Length || body[cursor] != ':')
{
repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length);
index = cursor;
continue;
}
repaired.Append(body, key + AddressKey.Length, cursor - key - AddressKey.Length);
repaired.Append(':');
cursor++;
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) cursor++;
if (cursor >= body.Length || body[cursor] != '"')
{
index = cursor;
continue;
}
var close = body.IndexOf('"', cursor + 1);
if (close < 0)
{
repaired.Append(body, cursor, body.Length - cursor);
break;
}
repaired.Append('"').Append(LoopbackProbeAddress).Append('"');
replaced++;
index = close + 1;
}
if (replaced == 0) return null;
note = replaced.ToString(CultureInfo.InvariantCulture) + " region probe address(es) pinned to "
+ LoopbackProbeAddress + " (the server's list points at retired hosts)";
return repaired.ToString();
}
///
/// The client's model for this route is a name -> row map
/// (Dictionary<string, ...>), so the flat member object the server answers
/// with makes its reader demand '{' where a string sits and throw
/// "expected:'{', actual:'\"none\"'" (a crash report per login). Wrapping the
/// flat object under the row key(s) the request named gives it the map it wants.
///
private static string RepairUserSegment(string query, string body, ref string note)
{
var trimmed = body.TrimStart();
if (trimmed.Length == 0 || trimmed[0] != '{') return null;
var keys = new List();
foreach (var value in QueryValues(query))
{
AddKey(keys, value);
AddKey(keys, "user_segment:" + value);
}
if (keys.Count == 0) AddKey(keys, "user_segment");
var repaired = new StringBuilder(body.Length * (keys.Count + 1) + 16);
repaired.Append('{');
for (var i = 0; i < keys.Count; i++)
{
if (i > 0) repaired.Append(',');
repaired.Append('"').Append(keys[i]).Append("\":").Append(body);
}
repaired.Append('}');
note = "userSegment answered as a name -> row map (" + string.Join(", ", keys) + ")";
return repaired.ToString();
}
private static void AddKey(List keys, string key)
{
if (string.IsNullOrEmpty(key)) return;
foreach (var existing in keys)
if (existing == key) return;
keys.Add(key);
}
internal static IEnumerable QueryValues(string query)
{
if (string.IsNullOrEmpty(query)) yield break;
foreach (var pair in query.TrimStart('?').Split('&'))
{
if (pair.Length == 0) continue;
var separator = pair.IndexOf('=');
var value = separator < 0 ? pair : pair.Substring(separator + 1);
value = Uri.UnescapeDataString(value).Trim();
if (value.Length == 0) continue;
yield return value;
}
}
private static bool EndsWith(string value, string suffix) =>
value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
}