using System.Reflection; using System.Runtime.Loader; using System.Security.Cryptography; using System.Text.Json; using Mono.Cecil; using Mono.Cecil.Cil; using Patch = XFESystem.Tools.JintCleanupPatch.Program; namespace XFESystem.JintCleanupPatch.Tests; public sealed class CleanupPatchTests { [Theory] [InlineData(false, false, null)] [InlineData(true, false, "Original")] [InlineData(true, true, "Cleanup")] [InlineData(false, true, "Cleanup")] public void ActualPatchedIlRunsCleanupExactlyOnceAndPreservesExceptionReplacement(bool throwBody, bool throwCleanup, string? error) { var fixture = Fixture.Create(); fixture.MakeAssembly(); Patch.Main([fixture.Path]); var context = new AssemblyLoadContext("CleanupFixture" + Guid.NewGuid(), true); try { var assembly = context.LoadFromAssemblyPath(fixture.Path); var type = assembly.GetType("Fixture.Scope")!; var method = type.GetMethod("Run")!; if (error is null) method.Invoke(null, [throwBody, throwCleanup]); else Assert.Equal(error, Assert.Throws(() => method.Invoke(null, [throwBody, throwCleanup])).InnerException!.Message); Assert.Equal(1, type.GetField("Cleanups")!.GetValue(null)); } finally { context.Unload(); } } [Fact] public void RepeatedPatchIsVerifiedIdempotentAndChangedReceiptIsRejected() { var fixture = Fixture.Create(); fixture.MakeAssembly(); Patch.Main([fixture.Path]); var hash = Hash(fixture.Path); Patch.Main([fixture.Path]); Assert.Equal(hash, Hash(fixture.Path)); using var receipt = JsonDocument.Parse(File.ReadAllText(fixture.Path + ".XfeCleanup.json")); Assert.False(receipt.RootElement.GetProperty("GlobalPackageCacheModified").GetBoolean()); File.WriteAllText(fixture.Path + ".XfeCleanup.json", "{\"AfterSha256\":\"changed\"}"); Assert.Throws(() => Patch.Main([fixture.Path])); Assert.Equal(hash, Hash(fixture.Path)); } [Fact] public void FilesOutsidePrivateObjAreRejectedBeforeAnyMutation() { var fixture = Fixture.Create(); fixture.MakeAssembly(); var outside = System.IO.Path.Combine(fixture.Root, "Jint.dll"); File.Copy(fixture.Path, outside); var hash = Hash(outside); Assert.Throws(() => Patch.Main([outside])); Assert.Equal(hash, Hash(outside)); } [Fact] public void UnsupportedCriticalCleanupIsRejectedWithOriginalFileIntact() { var fixture = Fixture.Create(); fixture.MakeAssembly(criticalUnsupported: true); var hash = Hash(fixture.Path); Assert.Throws(() => Patch.Main([fixture.Path])); Assert.Equal(hash, Hash(fixture.Path)); Assert.False(File.Exists(fixture.Path + ".XfeCleanup.json")); } [Fact] public void ReenteredTryAfterHandledExceptionResetsItsCleanupGuard() { var fixture = Fixture.Create(); fixture.MakeAssembly(loop: true); Patch.Main([fixture.Path]); var context = new AssemblyLoadContext("CleanupLoop" + Guid.NewGuid(), true); try { var type = context.LoadFromAssemblyPath(fixture.Path).GetType("Fixture.Scope")!; type.GetMethod("Run")!.Invoke(null, [false, false]); Assert.Equal(2, type.GetField("Cleanups")!.GetValue(null)); } finally { context.Unload(); } } private static string Hash(string path) => Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path))); private sealed class Fixture { private Fixture(string root, string path) { Root = root; Path = path; } public string Root { get; } public string Path { get; } public static Fixture Create() { var root = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "XfeJintCleanup", Guid.NewGuid().ToString("N")); var directory = System.IO.Path.Combine(root, "obj", "cosmos", "ref"); Directory.CreateDirectory(directory); File.WriteAllText(System.IO.Path.Combine(root, "XFESystem.Kernel.csproj"), ""); return new(root, System.IO.Path.Combine(directory, "Jint.dll")); } public void MakeAssembly(bool criticalUnsupported = false, bool loop = false) { using var assembly = AssemblyDefinition.CreateAssembly(new AssemblyNameDefinition("Jint", new Version(4, 11, 0, 0)), "Jint", ModuleKind.Dll); var module = assembly.MainModule; var type = new TypeDefinition(criticalUnsupported ? "Jint" : "Fixture", criticalUnsupported ? "Engine" : "Scope", Mono.Cecil.TypeAttributes.Public | Mono.Cecil.TypeAttributes.Abstract | Mono.Cecil.TypeAttributes.Sealed, module.TypeSystem.Object); module.Types.Add(type); var counter = new FieldDefinition("Cleanups", Mono.Cecil.FieldAttributes.Public | Mono.Cecil.FieldAttributes.Static, module.TypeSystem.Int32); type.Fields.Add(counter); var method = new MethodDefinition("Run", Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Static, module.TypeSystem.Void); method.Parameters.Add(new("throwBody", Mono.Cecil.ParameterAttributes.None, module.TypeSystem.Boolean)); method.Parameters.Add(new("throwCleanup", Mono.Cecil.ParameterAttributes.None, module.TypeSystem.Boolean)); type.Methods.Add(method); var il = method.Body.GetILProcessor(); var first = il.Create(OpCodes.Ldarg_0); var bodyNormal = il.Create(OpCodes.Nop); var handlerStart = il.Create(OpCodes.Ldsfld, counter); var handlerDone = il.Create(OpCodes.Endfinally); var done = il.Create(OpCodes.Ret); var exceptionConstructor = module.ImportReference(typeof(Exception).GetConstructor([typeof(string)])!); if (loop) { var iteration = new VariableDefinition(module.TypeSystem.Int32); method.Body.Variables.Add(iteration); method.Body.InitLocals = true; var loopStart = il.Create(OpCodes.Ldloc, iteration); var innerDone = il.Create(OpCodes.Leave, done); var outerCatch = il.Create(OpCodes.Pop); var afterCatch = il.Create(OpCodes.Ldloc, iteration); il.Append(loopStart); il.Append(il.Create(OpCodes.Brtrue, bodyNormal)); il.Append(il.Create(OpCodes.Ldstr, "Original")); il.Append(il.Create(OpCodes.Newobj, exceptionConstructor)); il.Append(il.Create(OpCodes.Throw)); il.Append(bodyNormal); il.Append(il.Create(OpCodes.Leave, innerDone)); il.Append(handlerStart); il.Append(il.Create(OpCodes.Ldc_I4_1)); il.Append(il.Create(OpCodes.Add)); il.Append(il.Create(OpCodes.Stsfld, counter)); il.Append(handlerDone); il.Append(innerDone); innerDone.Operand = afterCatch; il.Append(outerCatch); il.Append(il.Create(OpCodes.Leave, afterCatch)); il.Append(afterCatch); il.Append(il.Create(OpCodes.Ldc_I4_1)); il.Append(il.Create(OpCodes.Add)); il.Append(il.Create(OpCodes.Stloc, iteration)); il.Append(il.Create(OpCodes.Ldloc, iteration)); il.Append(il.Create(OpCodes.Ldc_I4_2)); il.Append(il.Create(OpCodes.Blt, loopStart)); il.Append(done); method.Body.ExceptionHandlers.Add(new(ExceptionHandlerType.Finally) { TryStart = loopStart, TryEnd = handlerStart, HandlerStart = handlerStart, HandlerEnd = innerDone }); method.Body.ExceptionHandlers.Add(new(ExceptionHandlerType.Catch) { TryStart = loopStart, TryEnd = outerCatch, HandlerStart = outerCatch, HandlerEnd = afterCatch, CatchType = module.ImportReference(typeof(Exception)) }); assembly.Write(Path); return; } il.Append(first); il.Append(il.Create(OpCodes.Brfalse, bodyNormal)); il.Append(il.Create(OpCodes.Ldstr, "Original")); il.Append(il.Create(OpCodes.Newobj, exceptionConstructor)); il.Append(il.Create(OpCodes.Throw)); il.Append(bodyNormal); il.Append(il.Create(OpCodes.Leave, done)); il.Append(handlerStart); il.Append(il.Create(OpCodes.Ldc_I4_1)); il.Append(il.Create(OpCodes.Add)); il.Append(il.Create(OpCodes.Stsfld, counter)); il.Append(il.Create(OpCodes.Ldarg_1)); il.Append(il.Create(OpCodes.Brfalse, criticalUnsupported ? done : handlerDone)); il.Append(il.Create(OpCodes.Ldstr, "Cleanup")); il.Append(il.Create(OpCodes.Newobj, exceptionConstructor)); il.Append(il.Create(OpCodes.Throw)); il.Append(handlerDone); il.Append(done); method.Body.ExceptionHandlers.Add(new(ExceptionHandlerType.Finally) { TryStart = first, TryEnd = handlerStart, HandlerStart = handlerStart, HandlerEnd = done }); assembly.Write(Path); } } }