using System; using System.Collections; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text.RegularExpressions; using AOT.Editor.WeChat; using Newtonsoft.Json; using UnityEditor; using UnityEngine; public static class VerifyYooAssetStripping { static readonly string[] TypeNames = { "YooAsset.WebNetworkFileSystem", "YooAsset.BuiltinFileSystem", "YooAsset.SandboxFileSystem" }; static string Project => Path.GetFullPath(Path.Combine(Application.dataPath, "..")); static string Output => Path.GetFullPath(Path.Combine(Project, "../Tools/output/yooasset_stripping")); public static string Before() { return Save("before", new { full = Inspect(Path.Combine(Project, "Library/ScriptAssemblies/YooAsset.dll")), stripped = Inspect(Path.Combine(Project, "Library/Bee/artifacts/WebGL/ManagedStripped/YooAsset.dll")) }); } public static string BuildAndVerify() { if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL) throw new Exception("Select WebGL before running this verification"); string gamePath = Path.Combine(Project, "Build/WeChat/Base/minigame/game.js"); string oldDataPath = StartupDataPath(gamePath); string oldDataHash = Hash(oldDataPath); string[] business = SnapshotBusinessBundles(); DateTime started = DateTime.UtcNow; string result = ExportWeChatBasePackage.Export(); if (!result.StartsWith("ok minigame=", StringComparison.Ordinal)) throw new Exception(result); string strippedPath = Path.Combine(Project, "Library/Bee/artifacts/WebGL/ManagedStripped/YooAsset.dll"); var stripped = Inspect(strippedPath); if (File.GetLastWriteTimeUtc(strippedPath) < started) throw new Exception("Stripped YooAsset DLL is stale"); if (stripped.Any(t => !t.defaultConstructor || !t.constructorBody)) throw new Exception("A reflected file system constructor was stripped"); string cpp = Path.Combine(Project, "Library/Bee/artifacts/WebGL/il2cppOutput/cpp"); bool nativeConstructor = Directory.GetFiles(cpp, "YooAsset*.cpp") .Any(p => File.ReadAllText(p).Contains("WebNetworkFileSystem__ctor")); if (!nativeConstructor) throw new Exception("Native WebNetworkFileSystem constructor was not emitted"); if (!business.SequenceEqual(SnapshotBusinessBundles())) throw new Exception("Business bundles changed unexpectedly"); string newDataPath = StartupDataPath(gamePath); return Save("after", new { passed = true, export = result, startedUtc = started, completedUtc = DateTime.UtcNow, stripped, nativeConstructor, oldStartupData = oldDataPath, startupData = newDataPath, oldStartupHash = oldDataHash, startupHash = Hash(newDataPath), startupDataUnchanged = oldDataHash == Hash(newDataPath), businessBundlesUnchanged = true, wechatRuntimeRetested = false }); } public sealed class TypeCheck { public string type; public bool typePresent; public bool defaultConstructor; public bool constructorBody; } static TypeCheck[] Inspect(string path) { // Burst embeds a second Cecil copy; select Unity's loaded Mono.Cecil explicitly. var cecil = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Mono.Cecil"); var definition = cecil.GetType("Mono.Cecil.AssemblyDefinition"); object assembly = definition.GetMethod("ReadAssembly", new[] { typeof(string) }).Invoke(null, new object[] { path }); using ((IDisposable)assembly) { object module = Property(assembly, "MainModule"); return TypeNames.Select(name => { object type = module.GetType().GetMethod("GetType", new[] { typeof(string) }).Invoke(module, new object[] { name }); var check = new TypeCheck { type = name, typePresent = type != null }; if (type != null) foreach (object method in (IEnumerable)Property(type, "Methods")) if ((string)Property(method, "Name") == ".ctor" && !(bool)Property(method, "HasParameters")) { check.defaultConstructor = true; check.constructorBody = (bool)Property(method, "HasBody"); } return check; }).ToArray(); } } static object Property(object instance, string name) => instance.GetType().GetProperty(name).GetValue(instance); static string StartupDataPath(string gamePath) { var match = Regex.Match(File.ReadAllText(gamePath), @"DATA_FILE_MD5:\s*'([0-9a-f]+)'"); if (!match.Success) throw new Exception("Cannot read DATA_FILE_MD5"); return Path.Combine(Project, "Build/WeChat/Base/webgl", match.Groups[1].Value + ".webgl.data.unityweb.bin.txt"); } static string Hash(string path) { using (var sha = SHA256.Create()) using (var stream = File.OpenRead(path)) return BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", "").ToLowerInvariant(); } static string[] SnapshotBusinessBundles() => Directory.GetFiles(Path.Combine(Application.dataPath, "StreamingAssets/yoo"), "*.bundle", SearchOption.AllDirectories) .OrderBy(p => p, StringComparer.Ordinal).Select(p => p + ":" + Hash(p)).ToArray(); static string Save(string name, object report) { Directory.CreateDirectory(Output); string json = JsonConvert.SerializeObject(report, Formatting.Indented); File.WriteAllText(Path.Combine(Output, name + ".json"), json); return json; } }