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<TargetInvocationException>(() => 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<InvalidOperationException>(() => 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<ArgumentException>(() => 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<InvalidOperationException>(() => 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"), "<Project />");
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);
}
}
}
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<TargetInvocationException>(() => 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<InvalidOperationException>(() => 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<ArgumentException>(() => 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<InvalidOperationException>(() => 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"), "<Project />");
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);
}
}
}