using System.Security.Cryptography; using System.Text.Json; using Jint; using Mono.Cecil; using Mono.Cecil.Cil; using Patch = XFESystem.Tools.JintCleanupPatch.Program; namespace XFESystem.JintCleanupPatch.Tests; public sealed class BoundTargetPatchTests { private const string OriginalHash = "acf8a1a9f98aa771c95c2b15b9b58f21de8b77f027e2a8d5ca92597e87752d9d"; [Fact] public void ActualHostRuntimeCarriesTheSameVerifiedCallableFix() { var loaded = typeof(Engine).Assembly; Assert.NotNull(loaded.GetType("XFESystem.JintBoundCallableTargetsV1")); using var receipt = JsonDocument.Parse(File.ReadAllText(loaded.Location + ".XfeBind.json")); Assert.Equal(Hash(loaded.Location), receipt.RootElement.GetProperty("AfterSha256").GetString()); Assert.Equal(OriginalHash, receipt.RootElement.GetProperty("PinnedOriginalSha256").GetString()); Assert.False(receipt.RootElement.GetProperty("GlobalPackageCacheModified").GetBoolean()); } [Theory] [InlineData("function sum(a,b,c){return this.base+a+b+c;} const first=sum.bind({base:10},1),second=first.bind({base:900},2);String(second(3))", "16")] [InlineData("class C {constructor(){this.value=42;this.method=this.method.bind(this);this.second=this.method.bind(this);} method(){return this.value;}} String(new C().second())", "42")] [InlineData("const proxy=new Proxy(function(a){return this.base+a;},{apply(target,receiver,args){return Reflect.apply(target,receiver,args)+1}}); String(proxy.bind({base:40},1)())", "42")] [InlineData("function C(a,b){this.value=a+b;} const A=C.bind(null,20),B=A.bind(null,22),object=new B();[object.value,object instanceof C,object instanceof A,object instanceof B].join(',')", "42,true,true,true")] [InlineData("let result='';try{Function.prototype.bind.call({},null);}catch(error){result=error.name;} result", "TypeError")] [InlineData("const a=(function(a,b,c){}).bind(null,1),b=a.bind(null,2);[a.length,b.length,a.name,b.name].join(',')", "2,1,bound ,bound bound ")] public void BoundCallsPreserveArgumentsReceiverConstructorsProxyAndErrors(string script, string expected) { var engine = new Engine(options => { options.Interop.Enabled = false; options.DisableStringCompilation(); options.MaxStatements(100_000); }); Assert.Equal(expected, engine.Evaluate(script).AsString()); } [Fact] public void ExactPinnedPrivateInputIsPatchedIdempotentlyWithoutTouchingGlobalPackage() { var source = Original(); var before = Hash(source); var path = PrivatePath(); File.Copy(source, path); Patch.Main([path, "--bind-only"]); var hash = Hash(path); Patch.Main([path, "--bind-only"]); Assert.Equal(hash, Hash(path)); Assert.Equal(before, Hash(source)); using var assembly = AssemblyDefinition.ReadAssembly(path); var type = assembly.MainModule.GetType("Jint.Native.Function.BindFunction"); var call = type.Methods.Single(method => method.Name.EndsWith("ICallable.Call", StringComparison.Ordinal)); Assert.Contains(call.Body.Instructions, instruction => instruction.OpCode == OpCodes.Isinst && instruction.Operand is TypeReference reference && reference.FullName == "Jint.Native.ICallable"); Assert.Contains(call.Body.Instructions, instruction => instruction.OpCode == OpCodes.Callvirt && instruction.Operand is MethodReference reference && reference.DeclaringType.FullName == "Jint.Native.ICallable"); Assert.DoesNotContain(call.Body.Instructions, instruction => instruction.OpCode == OpCodes.Isinst && instruction.Operand is TypeReference reference && reference.FullName == "Jint.Native.Function.Function"); } [Fact] public void ChangedUnmarkedInputAndMissingOrChangedReceiptAreRejectedBeforeMutation() { var path = PrivatePath(); File.Copy(Original(), path); var bytes = File.ReadAllBytes(path); bytes[^1] ^= 1; File.WriteAllBytes(path, bytes); var changed = Hash(path); Assert.Throws(() => Patch.Main([path, "--bind-only"])); Assert.Equal(changed, Hash(path)); File.Copy(Original(), path, true); Patch.Main([path, "--bind-only"]); var ready = Hash(path); File.WriteAllText(path + ".XfeBind.json", "{\"AfterSha256\":\"changed\"}"); Assert.Throws(() => Patch.Main([path, "--bind-only"])); Assert.Equal(ready, Hash(path)); File.Delete(path + ".XfeBind.json"); Assert.Throws(() => Patch.Main([path, "--bind-only"])); Assert.Equal(ready, Hash(path)); } [Fact] public void GlobalPackageAndNonPrivateWorkspaceFilesAreRejected() { var source = Original(); var before = Hash(source); Assert.Throws(() => Patch.Main([source, "--bind-only"])); Assert.Equal(before, Hash(source)); var outside = Path.Combine(Path.GetDirectoryName(PrivatePath())!, "Outside.dll"); File.Copy(source, outside); Assert.Throws(() => Patch.Main([outside, "--bind-only"])); Assert.Equal(before, Hash(outside)); } [Fact] public void NativeCombinedModeKeepsCleanupAndCallableReceiptsOnTheActualFinalHash() { var path = PrivatePath(native: true); File.Copy(Original(), path); Patch.Main([path, "--bind-and-cleanup"]); var hash = Hash(path); Patch.Main([path, "--bind-and-cleanup"]); Assert.Equal(hash, Hash(path)); foreach (var suffix in new[] { ".XfeCleanup.json", ".XfeBind.json" }) { using var receipt = JsonDocument.Parse(File.ReadAllText(path + suffix)); Assert.Equal(hash, receipt.RootElement.GetProperty("AfterSha256").GetString()); } } private static string Original() { var packages = Environment.GetEnvironmentVariable("NUGET_PACKAGES") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); var path = Path.Combine(packages, "jint", "4.11.0", "lib", "net10.0", "Jint.dll"); Assert.Equal(OriginalHash, Hash(path)); return path; } private static string PrivatePath(bool native = false) { string? root = AppContext.BaseDirectory; while (root is not null && !File.Exists(Path.Combine(root, "XFESystem.slnx"))) root = Path.GetDirectoryName(root); Assert.NotNull(root); var fixture = Path.Combine(root, "build", "CSharp", "JintBoundFixture-" + Guid.NewGuid().ToString("N")); var directory = native ? Path.Combine(fixture, "obj", "cosmos", "ref") : Path.Combine(fixture, "obj", "XfeJintHost"); Directory.CreateDirectory(directory); if (native) File.WriteAllText(Path.Combine(fixture, "XFESystem.Kernel.csproj"), ""); return Path.Combine(directory, "Jint.dll"); } private static string Hash(string path) => Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path))); }