summary refs log tree commit diff
path: root/ReferenceClientProxyImplementation/Patches/Implementations
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-02-23 02:03:20 +0100
committerRory& <root@rory.gay>2026-02-23 02:03:20 +0100
commit77a609758bb80bac9497d2e3988550f8be578407 (patch)
tree991a9d258ca4fece1132a1a344d0fe11e3b03d51 /ReferenceClientProxyImplementation/Patches/Implementations
downloadReferenceClientProxyImplementation-master.tar.xz
Initial commit HEAD master
Diffstat (limited to 'ReferenceClientProxyImplementation/Patches/Implementations')
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs91
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs72
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs22
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs36
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs32
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs21
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs21
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs26
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs26
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs33
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs79
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs16
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs32
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs34
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs32
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs56
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs27
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs19
-rw-r--r--ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs26
19 files changed, 701 insertions, 0 deletions
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs
new file mode 100644

index 0000000..649625f --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs
@@ -0,0 +1,91 @@ +using System.Diagnostics; +using ArcaneLibs; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations; + +public partial class FormatJsFilePatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => -100; + + public string GetName() => "Format JS file"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js") || relativeName.EndsWith(".css") || relativeName.EndsWith(".html"); + + public async Task<byte[]> Execute(string relativeName, byte[] content) { + var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName); + if (File.Exists(cachePath)) { + Console.WriteLine($"Using cached formatted file for {relativeName}"); + return await File.ReadAllBytesAsync(cachePath); + } + + // temporary: add some newlines + var stringContent = System.Text.Encoding.UTF8.GetString(content); + // stringContent = stringContent.Replace("function(){", "function() {\n"); + content = System.Text.Encoding.UTF8.GetBytes(stringContent); + + + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}"; + await File.WriteAllBytesAsync(tmpPath, content); + var sw = Stopwatch.StartNew(); + ProcessStartInfo psi; + + // Biome doesn't support HTML and struggles with upstream emitting Sass directives + if (relativeName.EndsWith(".html") || relativeName.EndsWith(".css")) + psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + else + psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") { + // psi = new ProcessStartInfo(config.AssetCache.BiomePath, $"format --write --line-width 240 --files-max-size=100000000 {tmpPath}") { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) { + throw new InvalidOperationException("Failed to start the formatting process."); + } + + // var stdout = await process.StandardOutput.ReadToEndAsync(); + // var stderr = await process.StandardError.ReadToEndAsync(); + // await process.WaitForExitAsync(); + + Dictionary<ulong, string> stdoutLines = new(); + Dictionary<ulong, string> stderrLines = new(); + + process.OutputDataReceived += (sender, args) => { + if (args.Data != null) { + stdoutLines[(ulong)sw.ElapsedMilliseconds] = args.Data; + Console.Write("O"); + } + }; + process.ErrorDataReceived += (sender, args) => { + if (args.Data != null) { + stderrLines[(ulong)sw.ElapsedMilliseconds] = args.Data; + Console.Write("E"); + } + }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(); + + Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + + if (process.ExitCode != 0) { + Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + throw new Exception($"Failed to exec({psi.FileName} {psi.Arguments}): {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}"); + } + + var result = await File.ReadAllBytesAsync(tmpPath); + File.Move(tmpPath, cachePath); + return result; + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs new file mode 100644
index 0000000..877a31a --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs
@@ -0,0 +1,72 @@ +using System.Diagnostics; +using ArcaneLibs; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations; + +public partial class FormatHtmlCssPatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => -100; + + public string GetName() => "Format HTML/CSS file"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".css") || relativeName.EndsWith(".html"); + + public async Task<byte[]> Execute(string relativeName, byte[] content) { + var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName); + if (File.Exists(cachePath)) { + Console.WriteLine($"Using cached formatted file for {relativeName}"); + return await File.ReadAllBytesAsync(cachePath); + } + + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}"; + await File.WriteAllBytesAsync(tmpPath, content); + var sw = Stopwatch.StartNew(); + ProcessStartInfo psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + if (process == null) { + throw new InvalidOperationException("Failed to start the formatting process."); + } + + // var stdout = await process.StandardOutput.ReadToEndAsync(); + // var stderr = await process.StandardError.ReadToEndAsync(); + // await process.WaitForExitAsync(); + + Dictionary<ulong, string> stdoutLines = new(); + Dictionary<ulong, string> stderrLines = new(); + + while (!process.HasExited) { + while (!process.StandardOutput.EndOfStream) { + var line = await process.StandardOutput.ReadLineAsync(); + if (line == null) continue; + stdoutLines[(ulong)sw.ElapsedMilliseconds] = line; + Console.Write("O"); + } + + while (!process.StandardError.EndOfStream) { + var line = await process.StandardError.ReadLineAsync(); + if (line == null) continue; + stderrLines[(ulong)sw.ElapsedMilliseconds] = line; + Console.Write("E"); + } + } + + // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + + if (process.ExitCode != 0) { + Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}"))); + throw new Exception($"Failed to format file {relativeName}: {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}"); + } + + var result = await File.ReadAllBytesAsync(tmpPath); + File.Move(tmpPath, cachePath); + return result; + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs new file mode 100644
index 0000000..dc54850 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs
@@ -0,0 +1,22 @@ +using System.Text; +using System.Text.RegularExpressions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches; + +public class GlobalEnvPatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch GLOBAL_ENV"; + public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html"; + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + foreach(var (key, value) in config.TestClient.GlobalEnv) { + stringContent = new Regex($"{key}: \".*?\"").Replace(stringContent, $"{key}: \"{value}\""); + } + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs new file mode 100644
index 0000000..6c5f312 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs
@@ -0,0 +1,36 @@ +using System.Text; +using System.Text.RegularExpressions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches; + +public partial class StripNoncesPatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => 0; + + public string GetName() => "Strip nonces/integrity from html"; + public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html"; + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + stringContent = HtmlScriptIntegrityRegex().Replace( + HtmlScriptNonceRegex().Replace( + JsElementNonceRegex().Replace( + stringContent, + "" + ), + "" + ), + "" + ); + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex("\\snonce=\"[a-zA-Z0-9+/=]+\"")] + private static partial Regex HtmlScriptNonceRegex(); + + [GeneratedRegex("\\w.nonce='[a-zA-Z0-9+/=]+';")] + private static partial Regex JsElementNonceRegex(); + + [GeneratedRegex(@"\sintegrity=""[a-zA-Z0-9+/=\-\s]+""")] + private static partial Regex HtmlScriptIntegrityRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs new file mode 100644
index 0000000..6e7f7a2 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs
@@ -0,0 +1,32 @@ +using System.Text; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public class ApiProtocolPatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => "API: Use GLOBAL_ENV.API_PROTOCOL instead of hardcoded https:"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + // TODO: regex + stringContent = stringContent + .Replace( + "return \"https:\" + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");", + "return window.GLOBAL_ENV.API_PROTOCOL + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");" + ) + .Replace( + "api_endpoint: \"\".concat(\"https:\").concat(window.GLOBAL_ENV.API_ENDPOINT)", + "api_endpoint: window.GLOBAL_ENV.API_PROTOCOL.concat(window.GLOBAL_ENV.API_ENDPOINT)" + ) + .Replace( + "f = null != d ? \"https://\".concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT,", + "f = null != d ? window.GLOBAL_ENV.API_PROTOCOL.concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT," + ) + ; + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs new file mode 100644
index 0000000..152a3f3 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs
@@ -0,0 +1,21 @@ +using System.Text; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public class BooleanPatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => "Use real booleans in JS files"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = stringContent + .Replace("return!", "return !") + .Replace("!0", "true") + .Replace("!1", "false"); + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs new file mode 100644
index 0000000..fe73c8e --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs
@@ -0,0 +1,21 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class BooleanPropagationPatch : IPatch { +// public int GetOrder() => 3; +// +// public string GetName() => "Patch pointless boolean comparisons in JS"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// var stringContent = Encoding.UTF8.GetString(content); +// +// stringContent = stringContent +// .Replace(" && true", "").Replace(" || false", "").Replace("false || ", "") +// ; +// +// return Encoding.UTF8.GetBytes(stringContent); +// } +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs new file mode 100644
index 0000000..c44bf95 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs
@@ -0,0 +1,26 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class DisableSciencePatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => @"JS(web): Disable /science calls"; + public bool Applies(string relativeName, byte[] content) => relativeName.StartsWith("assets/web.") && relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + var match = HandleTrackDefinitionRegex().Match(stringContent); + stringContent = stringContent.Insert(match.Index + match.Length, @" + return (new Promise(() => { }), false); // ReferenceClientProxyImplementation: Disable /science calls + "); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@".\.handleTrack = function \(.\) \{", RegexOptions.Compiled)] + private static partial Regex HandleTrackDefinitionRegex(); + +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs new file mode 100644
index 0000000..9eed00c --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs
@@ -0,0 +1,26 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class ExpandUnicodeEscapesPatch : IPatch { + public int GetOrder() => 0; + + public string GetName() => @"JS: expand \x?? to \u00??"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = XToURegex().Replace( + stringContent, + m => $"\\u00{m.Groups[1].Value}" + ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"\\x([0-9A-Fa-f]{2})", RegexOptions.Compiled)] + private static partial Regex XToURegex(); + +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs new file mode 100644
index 0000000..b741f56 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs
@@ -0,0 +1,33 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class IsStaffPatch : IPatch { + public int GetOrder() => 2; + + public string GetName() => "Patch isStaff/isStaffPersonal in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = IsNullableStaffRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}!!{m.Groups[2].Value}" + ); + + stringContent = IsStaffRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}true" + ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"(\W)(\w|this|\w\.user)\.isStaff(Personal)?\(\)", RegexOptions.Compiled)] + private static partial Regex IsStaffRegex(); + + [GeneratedRegex(@"(\W)(\w|this|\w\.user)\?\.isStaff(Personal)?\(\)", RegexOptions.Compiled)] + private static partial Regex IsNullableStaffRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs new file mode 100644
index 0000000..b5e7d77 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs
@@ -0,0 +1,79 @@ +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using ReferenceClientProxyImplementation.Configuration; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class JsonParseMultilinePatch(ProxyConfiguration config) : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch null-coalescing expressions in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + var matches = JsonParseRegex().Matches(stringContent); + Console.WriteLine($"Found {matches.Count} JSON.parse calls in {relativePath}"); + + + + await Parallel.ForEachAsync(matches, async (match, ct) => { + string formattedJson = match.Groups[1].Value; + try { + var jsonElement = JsonSerializer.Deserialize<JsonElement>(formattedJson.Replace("\\", "\\\\") + "waef"); + formattedJson = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions { WriteIndented = true }); + } catch (JsonException je) { + // Console.WriteLine($"STJ: Failed to parse JSON in {relativePath} at index {match.Index}: {je.Message}"); // intentinally broken + try { + formattedJson = await formatJsonWithNodejs(relativePath, match, ct); + } catch (Exception e) { + Console.WriteLine($"Node.js: Failed to parse JSON in {relativePath} at index {match.Index}: {e.Message}"); + return; + } + } + + lock (matches) stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\", "\\\\")}`);"); + }); + + return Encoding.UTF8.GetBytes(stringContent); + } + + private async Task<string> formatJsonWithNodejs(string relativePath, Match match, CancellationToken ct) { + // Extract the JSON string from the match + var id = "dcp_" + Path.GetFileName(relativePath).Replace('.', '_') + "_" + match.Index; + await File.WriteAllTextAsync($"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js", $"console.log(JSON.stringify(JSON.parse(`{match.Groups[1].Value.Replace("`", "\\\\\\`")}`), null, 2))"); + var sw = Stopwatch.StartNew(); + + var psi = new ProcessStartInfo(config.AssetCache.NodePath, $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + + using var process = Process.Start(psi); + if (process == null) { + throw new InvalidOperationException("Failed to start the formatting process."); + } + + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + + if (process.ExitCode != 0) { + Console.WriteLine($"Failed to run {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js in {sw.ElapsedMilliseconds}ms: {process.ExitCode}"); + Console.WriteLine("Standard Output: " + stdout); + Console.WriteLine("Standard Error: " + stderr); + throw new Exception($"Failed to execute {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js: {stderr}"); + } + + var formattedJson = stdout.Trim(); + Console.WriteLine($"Parsed JSON({id}) in {sw.ElapsedMilliseconds}ms: {formattedJson.Length} bytes"); + // stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\n", "\\\\n")}`);"); + await File.WriteAllTextAsync($"{config.TestClient.RevisionPath}/patched/assets/{Path.GetFileName(relativePath)}-{match.Index}.json", formattedJson); + return formattedJson; + } + + [GeneratedRegex(@"JSON\.parse\(\n\s*'(.*?)',?\s*\);", RegexOptions.Compiled | RegexOptions.Multiline)] + private static partial Regex JsonParseRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs new file mode 100644
index 0000000..a94e312 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs
@@ -0,0 +1,16 @@ +using System.Text; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public class KnownConstantsPatch : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Use named constants"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs new file mode 100644
index 0000000..e7f78a0 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs
@@ -0,0 +1,32 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class LegacyJsPathces : IPatch { +// public int GetOrder() => 1; +// +// public string GetName() => "Patch deprecated JS constructs"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// var stringContent = Encoding.UTF8.GetString(content); +// +// while(MozInputSourceRegex().IsMatch(stringContent)) { +// var match = MozInputSourceRegex().Match(stringContent); +// var replacement = match.Groups[1].Value switch { +// "0" => "", +// "1" => "mouse", +// "2" => "pen", +// "3" => "pen", +// "4" => "touch", +// _ => throw new InvalidOperationException("Unreachable") +// }; +// } +// +// return Encoding.UTF8.GetBytes(stringContent); +// } +// +// [GeneratedRegex(@"([0-6]) === (\w).mozInputSource", RegexOptions.Compiled)] +// private static partial Regex MozInputSourceRegex(); +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs new file mode 100644
index 0000000..2005c4c --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs
@@ -0,0 +1,34 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class LogErrorContextPatch : IPatch { +// public int GetOrder() => 2; +// +// public string GetName() => "Patch assertions to log more context"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// var stringContent = Encoding.UTF8.GetString(content); +// +// stringContent = NotNullAssertionRegex().Replace( +// stringContent, +// m => { +// var methodName = m.Groups[1].Value; +// var objectName = m.Groups[2].Value; +// var message = m.Groups[3].Value; +// Console.WriteLine($"Patching not-null assertion in {relativePath}: {methodName} - {message}"); +// +// return $@"{methodName}()(null != {objectName}, ""{message} - Context: "" + JSON.stringify({objectName}))"; +// } +// ); +// +// return Encoding.UTF8.GetBytes(stringContent); +// } +// +// // null assertion: u()(null != o, "PrivateChannel.renderAvatar: Invalid prop configuration - no user or channel"); +// // capture: method name, object name, message +// [GeneratedRegex(@"([a-zA-Z0-9_]+)\(\)\(\s*null != ([a-zA-Z0-9_]+),\s*""([^""]+)""\s*\)", RegexOptions.Compiled)] +// private static partial Regex NotNullAssertionRegex(); +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs new file mode 100644
index 0000000..98156c8 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs
@@ -0,0 +1,32 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class NullCoalescingPatch : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch null-coalescing expressions in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = NullCoalescingRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}?.{m.Groups[2].Value}" + ); + // stringContent = ParenNullCheckRegex().Replace( + // stringContent, + // m => $"{m.Groups[1].Value} == null" + // ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"null == ([a-zA-Z0-9_]+?) \? undefined : \1\.([a-zA-Z0-9_]+?)", RegexOptions.Compiled)] + private static partial Regex NullCoalescingRegex(); + + [GeneratedRegex(@"\(([^()]+?)\) == null", RegexOptions.Compiled)] + private static partial Regex ParenNullCheckRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs new file mode 100644
index 0000000..bab0756 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs
@@ -0,0 +1,56 @@ +// using System.Text; +// using System.Text.RegularExpressions; +// using ReferenceClientProxyImplementation.Services; +// +// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; +// +// public partial class PrefetchAssetsPatch(IServiceProvider sp) : IPatch { +// public int GetOrder() => 1000000; +// +// public string GetName() => "Prefetch assets"; +// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); +// +// private static SemaphoreSlim ss = new(2, 2); +// private static HashSet<string> alreadyKnownAssets = new(); +// +// public async Task<byte[]> Execute(string relativePath, byte[] content) { +// // Can't inject service due to loop +// var stringContent = Encoding.UTF8.GetString(content); +// var matches = PrefetchAssetsRegex().Matches(stringContent); +// +// Console.WriteLine($"Found {matches.Count} prefetch assets in {relativePath}"); +// if (matches.Count == 0) { +// return content; // No matches found, return original content +// } +// +// var clientStore = sp.GetRequiredService<ClientStoreService>(); +// +// var newAssets = matches +// .Select(x => x.Groups[1].Value) +// .Distinct() +// .Where(x => !clientStore.HasRawAsset(x) && alreadyKnownAssets.Add(x)); +// +// var tasks = newAssets +// .Select(async match => { +// await ss.WaitAsync(); +// Console.WriteLine($"Discovered prefetch asset in {relativePath}: {match}"); +// // var patches = sp.GetRequiredService<PatchSet>(); +// var res = await clientStore.GetOrDownloadRawAsset(match); +// await res.DisposeAsync(); +// ss.Release(); +// Console.WriteLine($"Prefetched asset {match} in {relativePath}"); +// }).ToList(); +// +// if (tasks.Count == 0) { +// Console.WriteLine($"No new prefetch assets found in {relativePath}, returning original content."); +// return content; // No new assets to prefetch, return original content +// } +// +// await Task.WhenAny(tasks); +// +// return content; +// } +// +// [GeneratedRegex(@".\.exports = ""((?:[a-z\d/\.]*?)\.\w{2,4})""", RegexOptions.Compiled)] +// private static partial Regex PrefetchAssetsRegex(); +// } \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs new file mode 100644
index 0000000..9d819c7 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs
@@ -0,0 +1,27 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class Void0Patch : IPatch { + public int GetOrder() => 0; + + public string GetName() => "Use literal undefined instead of void 0"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string _, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = stringContent + .Replace("void 0", "undefined"); + stringContent = VoidFunctionRegex().Replace( + stringContent, + m => $"{m.Groups[1].Value}(" + ); + + return Encoding.UTF8.GetBytes(stringContent); + } + + [GeneratedRegex(@"\(0, ([a-zA-Z0-9_.$]+?)\)\(", RegexOptions.Compiled)] + private static partial Regex VoidFunctionRegex(); +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs new file mode 100644
index 0000000..277bf8a --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs
@@ -0,0 +1,19 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches; + +public partial class WhileTruePatch : IPatch { + public int GetOrder() => 1; + + public string GetName() => "Patch while(true) expressions in JS"; + public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js"); + + public async Task<byte[]> Execute(string relativePath, byte[] content) { + var stringContent = Encoding.UTF8.GetString(content); + + stringContent = stringContent.Replace("for (;;)", "while (true)"); + + return Encoding.UTF8.GetBytes(stringContent); + } +} \ No newline at end of file diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs new file mode 100644
index 0000000..c3dba59 --- /dev/null +++ b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs
@@ -0,0 +1,26 @@ +namespace ReferenceClientProxyImplementation.Patches.Implementations; + +public class PatchSet(IServiceProvider sp) { + public List<IPatch> Patches { get; } = sp.GetServices<IPatch>().OrderBy(x => x.GetOrder()).ToList(); + + public async Task<byte[]> ApplyPatches(string relativeName, byte[] content) { + var i = 0; + var patches = Patches + .Where(p => p.Applies(relativeName, content)) + .OrderBy(p => p.GetOrder()) + .ToList(); + foreach (var patch in patches) { + if (patch.Applies(relativeName, content)) { + var defaultColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write("==> "); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"Running task {++i}/{patches.Count}: {patch.GetName()} (Type<{patch.GetType().Name}>)"); + Console.ForegroundColor = defaultColor; + content = await patch.Execute(relativeName, content); + } + } + + return content; + } +} \ No newline at end of file