// Copyright (c) 2026 Juhapekka Piiroinen // All Rights Reserved. // AmoosedWeb project: https://amoosed.com using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; const string sample = """ {"schema":1,"gameId":"my-indie-game","revision":7, "minGameVersion":"1.0.0","maxGameVersion":"2.0.0", "items":[{"id":"release-1","kind":"release","path":"game/releases.json"}]} """; if (Native.AbiRevision() != 16) throw new InvalidOperationException("Unsupported preview ABI revision"); const ulong requiredCapabilities = 0x03ff; if (Native.Capabilities(out ulong capabilities) != 0 || (capabilities & requiredCapabilities) != requiredCapabilities) throw new InvalidOperationException("The DLL lacks required Windows preview capabilities"); byte[] json = Encoding.UTF8.GetBytes(sample); byte[] error = new byte[128]; int status = Native.CatalogueParse(json, (ulong)json.Length, out nint catalogue, error, (ulong)error.Length); if (status != 0 || catalogue == 0) throw new InvalidOperationException($"Parse failed: {status}: {ErrorText(error)}"); try { if (Native.CatalogueRevision(catalogue, out ulong revision) != 0 || revision != 7) throw new InvalidOperationException("Revision mismatch"); if (Native.CatalogueItemCount(catalogue, out ulong count) != 0 || count != 1) throw new InvalidOperationException("Item count mismatch"); if (Native.CatalogueItemString(catalogue, 0, 3, 0, 0, out ulong required) != 0) throw new InvalidOperationException("Length query failed"); nint buffer = Marshal.AllocHGlobal(checked((int)required + 1)); try { if (Native.CatalogueItemString(catalogue, 0, 3, buffer, required + 1, out _) != 0 || Marshal.PtrToStringUTF8(buffer) != "game/releases.json") throw new InvalidOperationException("Path copy mismatch"); } finally { Marshal.FreeHGlobal(buffer); } } finally { Native.CatalogueRelease(catalogue); } byte[] invalid = Encoding.UTF8.GetBytes("{\"schema\":2}"); if (Native.CatalogueParse(invalid, (ulong)invalid.Length, out catalogue, error, (ulong)error.Length) != 9 || catalogue != 0 || string.IsNullOrEmpty(ErrorText(error))) throw new InvalidOperationException("Invalid catalogue was accepted"); string? origin = Environment.GetEnvironmentVariable("AMW_PREVIEW_SITE_ORIGIN"); if (origin is not null) { RunSite(origin, Required("AMW_PREVIEW_SITE_OUTPUT"), null, Required("AMW_PREVIEW_MEMBER_KEY_PATH"), Required("AMW_PREVIEW_MEMBER_OUTPUT")); string external = Required("AMW_PREVIEW_EXTERNAL_ORIGIN"); RunSite(external, Required("AMW_PREVIEW_EXTERNAL_OUTPUT"), File.ReadAllBytes(Required("AMW_PREVIEW_EXTERNAL_KEY_PATH"))); RunSite(Required("AMW_PREVIEW_PIXEL_ORIGIN"), Required("AMW_PREVIEW_PIXEL_OUTPUT"), null); RunSite(Required("AMW_PREVIEW_FRAME_ORIGIN"), Required("AMW_PREVIEW_FRAME_OUTPUT"), null, resourcePath: "/amoosed.com/app/game/frame-note.txt"); RunSite(Required("AMW_PREVIEW_AUDIO_ORIGIN"), Required("AMW_PREVIEW_AUDIO_OUTPUT"), null, resourcePath: "/amoosed.com/app/game/audio-note.txt"); await RunCancellation(Required("AMW_PREVIEW_SLOW_ORIGIN"), Required("AMW_PREVIEW_SLOW_ARM"), Required("AMW_PREVIEW_SLOW_MARKER"), Path.GetDirectoryName(Required("AMW_PREVIEW_SITE_OUTPUT"))!); await RunPublishRace(Required("AMW_PREVIEW_SLOW_ORIGIN"), Required("AMW_PREVIEW_SLOW_ARM"), Required("AMW_PREVIEW_SLOW_MARKER"), Path.GetDirectoryName(Required("AMW_PREVIEW_SITE_OUTPUT"))!); } Console.WriteLine("OK: .NET 10 caller exercised the preview C ABI" + (origin is null ? "" : " and verified hosted/external/pixel/frame/audio downloads")); static string Required(string name) => Environment.GetEnvironmentVariable(name) ?? throw new InvalidOperationException($"Missing {name}"); static async Task RunCancellation(string origin, string arm, string marker, string directory) { byte[] originBytes = Encoding.UTF8.GetBytes(origin); byte[] error = new byte[256]; if (Native.SiteOpenHosted(originBytes, (ulong)originBytes.Length, out nint site, error, (ulong)error.Length) != 0 || site == 0) throw new InvalidOperationException($"Slow site open failed: {ErrorText(error)}"); var target = Path.Combine(directory, $"cancelled-{Guid.NewGuid():N}.html"); try { File.WriteAllText(arm, "armed"); byte[] resource = Encoding.UTF8.GetBytes("/amoosed.com/app/index.html"); byte[] output = Encoding.UTF8.GetBytes(target); var pending = Task.Run(() => { int status = Native.SiteDownload(site, resource, (ulong)resource.Length, 0, 0, output, (ulong)output.Length, out ulong bytes, error, (ulong)error.Length); return (status, bytes); }); for (int i = 0; i < 500 && !File.Exists(marker) && !pending.IsCompleted; i++) await Task.Delay(20); bool inFlight = File.Exists(marker) && !pending.IsCompleted; Native.SiteCancel(site); var (status, bytes) = await pending; if (!inFlight || status != 7 || bytes != 0 || File.Exists(target) || Directory.GetFiles(directory, Path.GetFileName(target) + ".amw-*").Length != 0) throw new InvalidOperationException($"In-flight cancel failed: status {status}, bytes {bytes}"); } finally { File.Delete(arm); Native.SiteRelease(site); } } static async Task RunPublishRace(string origin, string arm, string marker, string directory) { byte[] originBytes = Encoding.UTF8.GetBytes(origin); byte[] error = new byte[256]; if (Native.SiteOpenHosted(originBytes, (ulong)originBytes.Length, out nint site, error, (ulong)error.Length) != 0 || site == 0) throw new InvalidOperationException($"Race site open failed: {ErrorText(error)}"); var target = Path.Combine(directory, $"raced-{Guid.NewGuid():N}.html"); try { File.Delete(marker); File.WriteAllText(arm, "armed"); byte[] resource = Encoding.UTF8.GetBytes("/amoosed.com/app/index.html"); byte[] output = Encoding.UTF8.GetBytes(target); var pending = Task.Run(() => { int status = Native.SiteDownload(site, resource, (ulong)resource.Length, 0, 0, output, (ulong)output.Length, out ulong bytes, error, (ulong)error.Length); return (status, bytes); }); for (int i = 0; i < 500 && !File.Exists(marker) && !pending.IsCompleted; i++) await Task.Delay(20); if (!File.Exists(marker) || pending.IsCompleted) { Native.SiteCancel(site); await pending; throw new InvalidOperationException("Download did not reach delayed carrier"); } File.WriteAllText(target, "keep"); var (status, bytes) = await pending; if (status != 8 || bytes != 0 || File.ReadAllText(target) != "keep" || Directory.GetFiles(directory, Path.GetFileName(target) + ".amw-*").Length != 0) throw new InvalidOperationException($"Existing output race failed: status {status}, bytes {bytes}"); Console.WriteLine("OK: C ABI reported ALREADY_EXISTS when output appeared during download"); } finally { File.Delete(arm); Native.SiteRelease(site); } } static void RunSite(string origin, string outputPath, byte[]? credential, string? memberKeyPath = null, string? memberOutput = null, string? resourcePath = null) { byte[] address = Encoding.UTF8.GetBytes(origin); byte[] error = new byte[256]; int status = credential is null ? Native.SiteOpenHosted(address, (ulong)address.Length, out nint opened, error, (ulong)error.Length) : Native.SiteOpenExternal(address, (ulong)address.Length, credential, (ulong)credential.Length, out opened, error, (ulong)error.Length); if (status != 0 || opened == 0) throw new InvalidOperationException($"Site open failed: {status}: {ErrorText(error)}"); try { if (Native.SiteResourceCount(opened, out ulong count) != 0 || count == 0 || Native.SiteEntryPath(opened, 0, 0, out ulong needed) != 0) throw new InvalidOperationException("Site catalogue is unavailable"); if (Native.SiteMemberKeyId(opened, 0, 0, out ulong memberIdLength) != 0 || memberIdLength != (ulong)"hello-members".Length) throw new InvalidOperationException("Verified member edition ID is unavailable"); nint memberIdBuffer = Marshal.AllocHGlobal(checked((int)memberIdLength + 1)); try { if (Native.SiteMemberKeyId(opened, memberIdBuffer, memberIdLength + 1, out _) != 0 || Marshal.PtrToStringUTF8(memberIdBuffer) != "hello-members") throw new InvalidOperationException("Verified member edition ID differs"); } finally { Marshal.FreeHGlobal(memberIdBuffer); } nint buffer = Marshal.AllocHGlobal(checked((int)needed + 1)); string entry; try { if (Native.SiteEntryPath(opened, buffer, needed + 1, out _) != 0) throw new InvalidOperationException("Entry path copy failed"); entry = Marshal.PtrToStringUTF8(buffer) ?? throw new InvalidOperationException("Missing entry"); } finally { Marshal.FreeHGlobal(buffer); } string selected = resourcePath ?? entry; ulong entryIndex = ulong.MaxValue; for (ulong index = 0; index < count; index++) { if (ReadSiteField(opened, index, 1) == selected) { entryIndex = index; break; } } if (entryIndex == ulong.MaxValue || Native.SiteResourceLength(opened, entryIndex, out ulong expectedLength) != 0 || expectedLength == 0 || ReadSiteField(opened, entryIndex, 3).Length == 0 || ReadSiteField(opened, entryIndex, 4).Length == 0 || ReadSiteField(opened, entryIndex, 5).Length == 0) throw new InvalidOperationException("Entry metadata is unavailable"); string expectedHash = ReadSiteField(opened, entryIndex, 2); byte[] resource = Encoding.UTF8.GetBytes(selected); byte[] target = Encoding.UTF8.GetBytes(outputPath); status = Native.SiteDownload(opened, resource, (ulong)resource.Length, 0, 0, target, (ulong)target.Length, out ulong bytes, error, (ulong)error.Length); if (status != 0 || bytes != expectedLength || (ulong)new FileInfo(outputPath).Length != bytes || !Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(outputPath))) .Equals(expectedHash, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"Verified download failed: {status}: {ErrorText(error)}"); if (memberKeyPath is not null && memberOutput is not null) { byte[] memberResource = Encoding.UTF8.GetBytes("/amoosed.com/app/game/bonus/note.txt"); byte[] memberTarget = Encoding.UTF8.GetBytes(memberOutput); status = Native.SiteDownload(opened, memberResource, (ulong)memberResource.Length, 0, 0, memberTarget, (ulong)memberTarget.Length, out ulong deniedBytes, error, (ulong)error.Length); if (status != 10 || deniedBytes != 0 || File.Exists(memberOutput)) throw new InvalidOperationException($"Missing member key returned {status} and {deniedBytes} bytes"); byte[] key = Convert.FromHexString(File.ReadAllText(memberKeyPath).Trim()); if (key.Length != 32) throw new InvalidOperationException("Invalid test member key"); nint keyMemory = Marshal.AllocHGlobal(32); try { key[0] ^= 1; Marshal.Copy(key, 0, keyMemory, 32); key[0] ^= 1; status = Native.SiteDownload(opened, memberResource, (ulong)memberResource.Length, keyMemory, 32, memberTarget, (ulong)memberTarget.Length, out deniedBytes, error, (ulong)error.Length); if (status != 10 || deniedBytes != 0 || File.Exists(memberOutput)) throw new InvalidOperationException($"Wrong member key returned {status} and {deniedBytes} bytes"); Marshal.Copy(key, 0, keyMemory, 32); status = Native.SiteDownload(opened, memberResource, (ulong)memberResource.Length, keyMemory, 32, memberTarget, (ulong)memberTarget.Length, out bytes, error, (ulong)error.Length); if (status != 0 || bytes == 0 || (ulong)new FileInfo(memberOutput).Length != bytes) throw new InvalidOperationException($"Member download failed: {status}: {ErrorText(error)}"); } finally { Marshal.Copy(new byte[32], 0, keyMemory, 32); Marshal.FreeHGlobal(keyMemory); Array.Clear(key); } } } finally { Native.SiteRelease(opened); } } static string ReadSiteField(nint site, ulong index, uint field) { if (Native.SiteResourceString(site, index, field, 0, 0, out ulong needed) != 0) throw new InvalidOperationException("Resource metadata length query failed"); nint memory = Marshal.AllocHGlobal(checked((int)needed + 1)); try { if (Native.SiteResourceString(site, index, field, memory, needed + 1, out _) != 0) throw new InvalidOperationException("Resource metadata copy failed"); return Marshal.PtrToStringUTF8(memory) ?? string.Empty; } finally { Marshal.FreeHGlobal(memory); } } static string ErrorText(byte[] bytes) { int end = Array.IndexOf(bytes, (byte)0); return Encoding.UTF8.GetString(bytes, 0, end < 0 ? bytes.Length : end); } static class Native { private const string Library = "AmoosedWebGameReaderC"; [DllImport(Library, EntryPoint = "amw_preview_abi_revision", CallingConvention = CallingConvention.Cdecl)] internal static extern uint AbiRevision(); [DllImport(Library, EntryPoint = "amw_preview_capabilities", CallingConvention = CallingConvention.Cdecl)] internal static extern int Capabilities(out ulong available); [DllImport(Library, EntryPoint = "amw_preview_catalogue_parse", CallingConvention = CallingConvention.Cdecl)] internal static extern int CatalogueParse(byte[] json, ulong length, out nint catalogue, byte[] error, ulong errorCapacity); [DllImport(Library, EntryPoint = "amw_preview_catalogue_release", CallingConvention = CallingConvention.Cdecl)] internal static extern void CatalogueRelease(nint catalogue); [DllImport(Library, EntryPoint = "amw_preview_catalogue_revision", CallingConvention = CallingConvention.Cdecl)] internal static extern int CatalogueRevision(nint catalogue, out ulong revision); [DllImport(Library, EntryPoint = "amw_preview_catalogue_item_count", CallingConvention = CallingConvention.Cdecl)] internal static extern int CatalogueItemCount(nint catalogue, out ulong count); [DllImport(Library, EntryPoint = "amw_preview_catalogue_item_string", CallingConvention = CallingConvention.Cdecl)] internal static extern int CatalogueItemString(nint catalogue, ulong index, uint field, nint output, ulong capacity, out ulong required); [DllImport(Library, EntryPoint = "amw_preview_site_open_hosted", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteOpenHosted(byte[] origin, ulong length, out nint site, byte[] error, ulong errorCapacity); [DllImport(Library, EntryPoint = "amw_preview_site_open_external", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteOpenExternal(byte[] origin, ulong length, byte[] credential, ulong credentialLength, out nint site, byte[] error, ulong errorCapacity); [DllImport(Library, EntryPoint = "amw_preview_site_release", CallingConvention = CallingConvention.Cdecl)] internal static extern void SiteRelease(nint site); [DllImport(Library, EntryPoint = "amw_preview_site_cancel", CallingConvention = CallingConvention.Cdecl)] internal static extern void SiteCancel(nint site); [DllImport(Library, EntryPoint = "amw_preview_site_resource_count", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteResourceCount(nint site, out ulong count); [DllImport(Library, EntryPoint = "amw_preview_site_entry_path", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteEntryPath(nint site, nint output, ulong capacity, out ulong required); [DllImport(Library, EntryPoint = "amw_preview_site_member_key_id", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteMemberKeyId(nint site, nint output, ulong capacity, out ulong required); [DllImport(Library, EntryPoint = "amw_preview_site_resource_length", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteResourceLength(nint site, ulong index, out ulong length); [DllImport(Library, EntryPoint = "amw_preview_site_resource_string", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteResourceString(nint site, ulong index, uint field, nint output, ulong capacity, out ulong required); [DllImport(Library, EntryPoint = "amw_preview_site_download", CallingConvention = CallingConvention.Cdecl)] internal static extern int SiteDownload(nint site, byte[] resource, ulong resourceLength, nint memberKey, ulong memberKeyLength, byte[] outputPath, ulong outputPathLength, out ulong bytes, byte[] error, ulong errorCapacity); }