using System.IO.Compression; using System.Net; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Text.Json.Nodes; using Avalonia; using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Media.Imaging; using Avalonia.Threading; using PCL.Communist; using PCL.Communist.Core; using PCL.Communist.ViewModels; using PCL.Communist.Views; var root = Path.Combine(Path.GetTempPath(), "pcl-cross-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); var windows = new PlatformInfo(PlatformKind.Windows, Architecture.X64, "10.0"); var mac = new PlatformInfo(PlatformKind.MacOS, Architecture.X64, "14.0"); var apple = new PlatformInfo(PlatformKind.MacOS, Architecture.Arm64, "14.0"); var linux = new PlatformInfo(PlatformKind.Linux, Architecture.X64, "6.1"); var android = new PlatformInfo(PlatformKind.Android, Architecture.Arm64, "14"); var ios = new PlatformInfo(PlatformKind.IOS, Architecture.Arm64, "17"); var count = 0; void Check(bool value, string message) { if (!value) throw new Exception("FAIL: " + message); count++; Console.WriteLine("PASS: " + message); } void Throws(Action action, string message) where T : Exception { try { action(); } catch (T) { Check(true, message); return; } throw new Exception("FAIL: " + message); } async Task ThrowsAsync(Func action, string message) where T : Exception { try { await action(); } catch (T) { Check(true, message); return; } throw new Exception("FAIL: " + message); } JsonObject Json(string json) => JsonNode.Parse(json)!.AsObject(); string Hash(byte[] bytes) => Convert.ToHexString(SHA1.HashData(bytes)).ToLowerInvariant(); JsonObject DownloadNode(string url, byte[] data, string? path = null) { var node = new JsonObject { ["url"] = url, ["sha1"] = Hash(data), ["size"] = data.Length }; if (path is not null) node["path"] = path; return node; } byte[] NativeZip(string fileName) { using var stream = new MemoryStream(); using (var zip = new ZipArchive(stream, ZipArchiveMode.Create, true)) { using (var writer = new StreamWriter(zip.CreateEntry(fileName).Open())) writer.Write("native-fixture"); using (var writer = new StreamWriter(zip.CreateEntry("META-INF/ignored").Open())) writer.Write("excluded"); } return stream.ToArray(); } try { Check(MinecraftRules.Allows(null, mac), "unconditional libraries are allowed"); var rules = JsonNode.Parse("""[{"action":"allow"},{"action":"disallow","os":{"name":"windows"}}]"""); Check(!MinecraftRules.Allows(rules, windows) && MinecraftRules.Allows(rules, mac), "rules use the last matching action"); var armRules = JsonNode.Parse("""[{"action":"allow","os":{"name":"osx","arch":"aarch64"}}]"""); Check(MinecraftRules.Allows(armRules, apple) && !MinecraftRules.Allows(armRules, mac), "Apple Silicon and Intel rules are distinct"); var features = JsonNode.Parse("""[{"action":"allow","features":{"is_demo_user":true}}]"""); Check(!MinecraftRules.Allows(features, linux), "demo and quick-play features default to disabled"); var nativeDefinition = Json("""{"natives":{"windows":"natives-windows-${arch}","osx":"natives-osx","linux":"natives-linux"}}"""); Check(MinecraftRules.NativeClassifier(nativeDefinition, mac) == "natives-osx", "macOS selects macOS natives"); Check(MinecraftRules.NativeClassifier(nativeDefinition, windows) == "natives-windows-64", "Windows classifier architecture substitution"); Throws(() => MinecraftRules.NativeClassifier(nativeDefinition, android), "mobile cannot select desktop natives"); Check(JavaRuntimeService.RuntimePlatform(apple) == "mac-os-arm64" && JavaRuntimeService.RuntimePlatform(mac) == "mac-os", "Java runtime keys distinguish Mac architectures"); Throws(() => JavaRuntimeService.RuntimePlatform(ios), "iOS cannot download a desktop JVM"); Check(AccountSession.Offline("Notch").Uuid == "b50ad385829d3141a2167e7d7539ba7f", "offline UUID matches Java nameUUIDFromBytes"); Throws(() => AccountSession.Offline("bad name;"), "offline player names are validated"); foreach (var unsafePath in new[] { "../escape", "/absolute", "C:/windows", "dir\\file", "a/../../b" }) Throws(() => LauncherPaths.SafeChild(root, unsafePath), "reject path: " + unsafePath); var metadata = Json("""{"id":"fixture","mainClass":"net.minecraft.client.main.Main","libraries":[{"downloads":{"artifact":{"path":"example/lib/1/lib.jar"}}}],"arguments":{"jvm":["-Djava.library.path=${natives_directory}","-cp","${classpath}",{"rules":[{"action":"allow","os":{"name":"windows"}}],"value":"-Dos.name=Windows 10"}],"game":["--username","${auth_player_name}","--gameDir","${game_directory}",{"rules":[{"action":"allow","features":{"is_demo_user":true}}],"value":"--demo"}]}}"""); var spacedPaths = new LauncherPaths(Path.Combine(root, "game with spaces")); var account = AccountSession.Offline("Player"); var macPlan = GameLauncher.BuildPlan(metadata, mac, spacedPaths, "/jdk/bin/java", account, 2048); var windowsPlan = GameLauncher.BuildPlan(metadata, windows, spacedPaths, "java.exe", account, 2048); Check(macPlan.Arguments.Contains("-XstartOnFirstThread") && !macPlan.Arguments.Contains("-Dos.name=Windows 10"), "macOS starts Java on the first thread without Windows flags"); Check(windowsPlan.Arguments.Contains("-Dos.name=Windows 10") && !windowsPlan.Arguments.Contains("-XstartOnFirstThread"), "Windows JVM arguments stay intact"); Check(macPlan.Arguments[3].Contains(":") && windowsPlan.Arguments[3].Contains(";"), "classpath separators follow the target OS"); Check(macPlan.CreateStartInfo().ArgumentList.Contains(spacedPaths.Root) && !macPlan.CreateStartInfo().UseShellExecute, "paths with spaces use argument lists without a shell"); Check(!macPlan.Arguments.Contains("--demo") && !macPlan.Arguments.Any(a => a.Contains("${")), "conditional arguments and placeholders are resolved"); Throws(() => GameLauncher.BuildPlan(metadata, ios, spacedPaths, "java", account, 2048), "iOS is prevented from invoking desktop Java"); Throws(() => GameLauncher.BuildPlan(metadata, mac, spacedPaths, "java", account with { ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(-1) }, 2048), "expired sessions require login"); var bytes = Encoding.UTF8.GetBytes("verified content"); var badBytes = Encoding.UTF8.GetBytes("corrupted mirror"); var handler = new FakeHandler(request => request.RequestUri!.Host == "bmclapi2.bangbang93.com" ? badBytes : bytes); using (var http = new HttpClient(handler)) { var downloader = new DownloadClient(http); var target = Path.Combine(root, "download.bin"); await downloader.FetchAsync(new("https://libraries.minecraft.net/test", target, Hash(bytes), bytes.Length)); Check(File.ReadAllBytes(target).SequenceEqual(bytes) && handler.Requests.Count == 2, "corrupt mirror falls back to verified official bytes"); await downloader.FetchAsync(new("https://libraries.minecraft.net/test", target, Hash(bytes), bytes.Length)); Check(handler.Requests.Count == 2, "valid cached downloads skip the network"); Check(!Directory.EnumerateFiles(root, "*.part", SearchOption.AllDirectories).Any(), "downloads leave no partial files"); Throws(() => downloader.Sources("http://insecure.example/file").ToArray(), "insecure download URLs are rejected"); using var cancel = new CancellationTokenSource(); cancel.Cancel(); await ThrowsAsync(() => downloader.FetchAsync(new("https://example.test/file", Path.Combine(root, "cancelled.bin")), cancel.Token), "cancelled downloads do not publish files"); } var zipPath = Path.Combine(root, "natives.zip"); await File.WriteAllBytesAsync(zipPath, NativeZip("liblwjgl.dylib")); var nativeRoot = Path.Combine(root, "native-output"); MinecraftInstaller.ExtractNatives(zipPath, nativeRoot, ["META-INF/"]); Check(File.Exists(Path.Combine(nativeRoot, "liblwjgl.dylib")) && !Directory.Exists(Path.Combine(nativeRoot, "META-INF")), "native extraction honors exclusions"); await File.WriteAllBytesAsync(zipPath, NativeZip("../escape.dll")); Throws(() => MinecraftInstaller.ExtractNatives(zipPath, nativeRoot, []), "native ZIP cannot escape its destination"); var assetBytes = Encoding.UTF8.GetBytes("asset-data"); var assetHash = Hash(assetBytes); var indexBytes = Encoding.UTF8.GetBytes(new JsonObject { ["objects"] = new JsonObject { ["sound/example.ogg"] = new JsonObject { ["hash"] = assetHash, ["size"] = assetBytes.Length } }, ["virtual"] = true }.ToJsonString()); var macZip = NativeZip("libfixture.dylib"); var winZip = NativeZip("fixture.dll"); var client = Encoding.UTF8.GetBytes("client-jar"); var library = Encoding.UTF8.GetBytes("library-jar"); var installMetadata = new JsonObject { ["id"] = "fixture", ["mainClass"] = "net.minecraft.client.main.Main", ["downloads"] = new JsonObject { ["client"] = DownloadNode("https://fixture.test/client", client) }, ["assetIndex"] = DownloadNode("https://fixture.test/index", indexBytes), ["libraries"] = new JsonArray(new JsonObject { ["name"] = "fixture:natives:1", ["natives"] = nativeDefinition["natives"]!.DeepClone(), ["downloads"] = new JsonObject { ["artifact"] = DownloadNode("https://fixture.test/lib", library, "fixture/lib.jar"), ["classifiers"] = new JsonObject { ["natives-osx"] = DownloadNode("https://fixture.test/mac", macZip, "fixture/mac.jar"), ["natives-windows-64"] = DownloadNode("https://fixture.test/win", winZip, "fixture/win.jar") } } }) }; installMetadata["assetIndex"]!["id"] = "fixture"; var metadataBytes = Encoding.UTF8.GetBytes(installMetadata.ToJsonString()); var responses = new Dictionary { ["/metadata"] = metadataBytes, ["/client"] = client, ["/index"] = indexBytes, ["/lib"] = library, ["/mac"] = macZip, ["/win"] = winZip, [$"/{assetHash[..2]}/{assetHash}"] = assetBytes }; var installHandler = new FakeHandler(request => responses[request.RequestUri!.AbsolutePath]); using (var http = new HttpClient(installHandler)) { var downloader = new DownloadClient(http, false); var version = new MinecraftVersion("fixture", "release", "https://fixture.test/metadata", Hash(metadataBytes)); var paths = new LauncherPaths(Path.Combine(root, "mac-install")); await new MinecraftInstaller(downloader, paths).InstallAsync(version, mac); Check(File.Exists(Path.Combine(paths.VersionDirectory("fixture"), "natives", "MacOS-X64", "libfixture.dylib")), "Mac installation extracts the correct native library"); Check(!installHandler.Requests.Any(u => u.AbsolutePath == "/win"), "Mac installation never downloads Windows natives"); Check(File.Exists(Path.Combine(paths.Assets, "virtual", "fixture", "sound", "example.ogg")), "legacy virtual assets are materialized"); await File.WriteAllTextAsync(Path.Combine(paths.Libraries, "fixture", "lib.jar"), "corrupt cached library"); await new MinecraftInstaller(downloader, paths).RepairAsync("fixture", mac); Check(File.ReadAllBytes(Path.Combine(paths.Libraries, "fixture", "lib.jar")).SequenceEqual(library), "repair revalidates and replaces corrupt local libraries"); Throws(() => MinecraftRules.ValidateNativeArchitecture(installMetadata, apple), "legacy Intel natives are rejected on Apple Silicon before installation"); var beforeMobile = installHandler.Requests.Count; var mobilePaths = new LauncherPaths(Path.Combine(root, "mobile-install")); await new MinecraftInstaller(downloader, mobilePaths).InstallAsync(version, android); Check(!Directory.Exists(mobilePaths.Libraries) && !installHandler.Requests.Skip(beforeMobile).Any(u => u.AbsolutePath is "/win" or "/mac"), "mobile downloads resources without desktop libraries"); var mobileRuntime = new GameLauncher(android, mobilePaths, new JavaRuntimeService(downloader, mobilePaths)); Check(!mobileRuntime.Capability.CanLaunch, "mobile runtime capability reports unavailable honestly"); await ThrowsAsync(() => mobileRuntime.LaunchAsync(metadata, account, null, 2048, null, CancellationToken.None), "mobile launch fails before any Java execution"); } var loginRequests = new List(); using (var http = new HttpClient(new FakeHandler(request => { loginRequests.Add(request.RequestUri!.Host + request.RequestUri.AbsolutePath); var response = request.RequestUri.AbsolutePath switch { "/consumers/oauth2/v2.0/devicecode" => """{"user_code":"ABCD1234","device_code":"private-device","verification_uri":"https://microsoft.com/link","expires_in":600,"interval":1}""", "/consumers/oauth2/v2.0/token" => """{"access_token":"oauth-private"}""", "/user/authenticate" => """{"Token":"xbl-private"}""", "/xsts/authorize" => """{"Token":"xsts-private","DisplayClaims":{"xui":[{"uhs":"user-hash"}]}}""", "/authentication/login_with_xbox" => """{"access_token":"mc-private","expires_in":3600}""", "/entitlements/mcstore" => """{"items":[{"name":"game_minecraft"}]}""", "/minecraft/profile" => """{"name":"TestPlayer","id":"00000000000000000000000000000000"}""", _ => throw new Exception("Unexpected authentication request") }; return Encoding.UTF8.GetBytes(response); }))) { DeviceCodePrompt? received = null; var session = await new MicrosoftLogin(http).LoginAsync(prompt => received = prompt); Check(received?.Code == "ABCD1234" && session.Name == "TestPlayer" && session.UserType == "msa", "device-code login completes Xbox, entitlement and Minecraft profile exchanges"); Check(loginRequests.IndexOf("api.minecraftservices.com/entitlements/mcstore") < loginRequests.IndexOf("api.minecraftservices.com/minecraft/profile"), "game ownership is checked before accepting the profile"); Check(session.ExpiresAt > DateTimeOffset.UtcNow.AddMinutes(50), "Microsoft sessions track token expiration"); } if (args.Contains("--network")) { using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(3) }; var downloader = new DownloadClient(http, false); var installer = new MinecraftInstaller(downloader, new LauncherPaths(Path.Combine(root, "network"))); var versions = await installer.GetVersionsAsync(); var version = versions.Single(v => v.Id == "1.21.1"); var versionPath = Path.Combine(root, "real-version.json"); await downloader.FetchAsync(new(version.Url, versionPath, version.Sha1)); var real = JsonNode.Parse(await File.ReadAllTextAsync(versionPath))!.AsObject(); var clientPath = Path.Combine(root, "real-client.jar"); await downloader.FetchAsync(MinecraftInstaller.Artifact(real["downloads"]!["client"]!, clientPath)); Check(new FileInfo(clientPath).Length > 10000000, "real Minecraft 1.21.1 client download passes SHA1 validation"); foreach (var platform in new[] { windows, mac, apple, linux }) { var plan = GameLauncher.BuildPlan(real, platform, spacedPaths, "java", account, 2048); Check(plan.Arguments.Count > 15 && !plan.Arguments.Any(a => a.Contains("${")), "real launch arguments resolve for " + platform.DisplayName); } if (args.Contains("--java")) { var java = await new JavaRuntimeService(downloader, new LauncherPaths(Path.Combine(root, "runtime-test"))).ResolveAsync(real, PlatformInfo.Current, null); Check(java.Major == 21, "official Java 21 runtime installs and executes on the host"); } } // Headless rendering exercises the same compiled XAML and bindings as every native entry point. AppBuilder.Configure().UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }).UseSkia().WithInterFont().SetupWithoutStarting(); var output = Path.GetFullPath(args.FirstOrDefault(a => a.StartsWith("--screenshots="))?.Split('=', 2)[1] ?? Path.Combine(root, "screenshots")); Directory.CreateDirectory(output); foreach (var (platform, width, height, file) in new[] { (mac, 820, 1000, "desktop.png"), (android, 390, 1100, "android.png"), (ios, 390, 1100, "ios.png") }) { var model = new MainViewModel(platform, new LauncherPaths(Path.Combine(root, file, "minecraft"))); model.Versions.Add(new("1.21.1", "release", "https://example.test/version", "")); model.SelectedVersion = model.Versions[0]; Check(model.LaunchCommand.CanExecute(null) == !platform.IsMobile, "launch button capability matches " + platform.DisplayName); Check(model.Themes.Count == 17, "all 17 theme choices are available on " + platform.DisplayName); var window = new Window { Width = width, Height = height, Content = new MainView { DataContext = model } }; window.Show(); Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); using var frame = window.CaptureRenderedFrame() ?? throw new Exception("Could not render " + file); frame.Save(Path.Combine(output, file), PngBitmapEncoderOptions.Default); Check(frame.PixelSize.Width == width, "rendered responsive view: " + file); window.Close(); } Console.WriteLine($"Completed {count} checks. Screenshots: {output}"); } finally { Directory.Delete(root, true); } sealed class FakeHandler(Func respond) : HttpMessageHandler { public List Requests { get; } = []; protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); byte[] content; lock (Requests) { Requests.Add(request.RequestUri!); content = respond(request); } return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(content) }); } }