// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.Serialization;
using System.Windows.Forms;
namespace ICSharpCode.ILSpy.Tests.Windows;
///
/// Builds instances at runtime and emits the
/// exact NRBF wire bytes that ResourceSerializedObject.GetBytes() would return for
/// such an entry in a real .resources stream. No binaries are checked in — every
/// fixture exists only in memory for the lifetime of one test.
///
///
/// Why this exists at all: BinaryFormatter is removed from net9+, so we can't ask
/// the BCL to serialise the streamer for us. We call the streamer's
/// to extract its single byte[] Data member,
/// then hand-write a minimal but spec-conformant NRBF envelope around that byte[] — the
/// same envelope BinaryFormatter would have written on .NET Framework.
///
internal static class ImageListFixtures
{
/// Single source of truth for the test bitmap drawn into each frame.
public static Bitmap BuildFrame(int width, int height, int frameIndex, ColorDepth depth)
{
var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(bmp))
{
// Each frame gets a different hue so cross-frame mismatches are loud, plus an
// X across the tile to exercise the alpha path on 32 bpp ImageLists.
byte hue = (byte)((frameIndex * 37) & 0xFF);
g.Clear(Color.FromArgb(255, hue, (byte)(255 - hue), 128));
using var pen = new Pen(Color.Black, 1f);
g.DrawLine(pen, 0, 0, width - 1, height - 1);
g.DrawLine(pen, 0, height - 1, width - 1, 0);
}
// Punch a transparent pixel so the alpha path has something distinctive.
// Skip palettised modes — those will quantise the alpha away.
if (depth == ColorDepth.Depth32Bit)
bmp.SetPixel(0, 0, Color.FromArgb(0, 0, 0, 0));
return bmp;
}
public sealed record Fixture(
byte[] NrbfBlob,
string TypeName,
Size FrameSize,
Bitmap[] Frames,
ColorDepth Depth,
bool HasMask) : IDisposable
{
public void Dispose()
{
foreach (var f in Frames)
f.Dispose();
}
}
public static Fixture Build(ColorDepth depth, bool withMask, int count, int frameSize = 16)
{
var list = new System.Windows.Forms.ImageList {
ColorDepth = depth,
ImageSize = new Size(frameSize, frameSize),
TransparentColor = withMask ? Color.Magenta : Color.Transparent,
};
// Sources stay alive until after the strip is materialised — ImageList.Add only
// keeps a reference to the source bitmap and copies into the strip lazily.
var sources = new Bitmap[count];
for (int i = 0; i < count; i++)
{
sources[i] = BuildFrame(frameSize, frameSize, i, depth);
list.Images.Add(sources[i]);
}
var streamer = list.ImageStream
?? throw new InvalidOperationException("ImageList.ImageStream was null after adding frames.");
// Capture frames AS STORED IN the strip. Sub-32 depths re-quantise so this is the
// real ground truth for byte comparison; comparing against the originals would
// measure WinForms's palettisation drift instead of decoder correctness.
var frames = new Bitmap[count];
for (int i = 0; i < count; i++)
frames[i] = new Bitmap(list.Images[i]);
foreach (var s in sources)
s.Dispose();
// Pull the raw "Data" byte[] out of the streamer via the ISerializable surface.
// On .NET 10 we can't ask BinaryFormatter to do this for us, but ImageListStreamer
// still implements ISerializable so GetObjectData is callable directly.
// SYSLIB0050 marks the whole formatter-serialisation surface obsolete — fine here,
// that's the only path the BCL offers for direct ISerializable invocation.
#pragma warning disable SYSLIB0050
var info = new SerializationInfo(typeof(ImageListStreamer), new FormatterConverter());
((ISerializable)streamer).GetObjectData(info, new StreamingContext(StreamingContextStates.All));
#pragma warning restore SYSLIB0050
byte[] data = (byte[])info.GetValue("Data", typeof(byte[]))!;
string typeName = typeof(ImageListStreamer).FullName!;
string libraryName = typeof(ImageListStreamer).Assembly.FullName!;
byte[] nrbf = BuildNrbfClassWithByteArrayMember(typeName, libraryName, "Data", data);
list.Dispose();
return new Fixture(nrbf, typeName, new Size(frameSize, frameSize), frames, depth, withMask);
}
// Records from [MS-NRBF] §2.1.2 (RecordTypeEnumeration). Spelt out as constants here
// instead of referencing the BCL enum because System.Formats.Nrbf doesn't expose the
// writer side — only the reader.
const byte RecSerializationHeader = 0;
const byte RecClassWithMembersAndTypes = 5;
const byte RecMemberReference = 9;
const byte RecMessageEnd = 11;
const byte RecBinaryLibrary = 12;
const byte RecArraySinglePrimitive = 15;
const byte BinaryTypePrimitiveArray = 7;
const byte PrimitiveTypeByte = 2; // PrimitiveTypeEnumeration.Byte per [MS-NRBF] §2.1.2.3 (Boolean=1, Byte=2)
///
/// Emits the smallest valid NRBF stream representing a single
/// SerializationInfo-style class with one byte[] member. Mirrors what
/// BinaryFormatter on .NET Framework would have produced for an ImageListStreamer.
///
public static byte[] BuildNrbfClassWithByteArrayMember(string className, string libraryName, string memberName, byte[] data)
{
const int rootObjectId = 1;
const int arrayObjectId = 2;
const int libraryId = 3;
using var ms = new MemoryStream();
using var w = new BinaryWriter(ms);
// SerializationHeaderRecord — §2.6.1.
w.Write(RecSerializationHeader);
w.Write(rootObjectId);
w.Write(-1); // headerId — opaque on read
w.Write(1); // majorVersion
w.Write(0); // minorVersion
// BinaryLibrary — §2.6.2. Must precede the ClassWithMembersAndTypes that
// references it, even though logically the class "owns" the library reference.
w.Write(RecBinaryLibrary);
w.Write(libraryId);
w.Write(libraryName);
// ClassWithMembersAndTypes — §2.3.2.2. ClassInfo + MemberTypeInfo + LibraryId,
// followed by member values.
w.Write(RecClassWithMembersAndTypes);
w.Write(rootObjectId); // ClassInfo.ObjectId
w.Write(className); // ClassInfo.Name
w.Write(1); // ClassInfo.MemberCount
w.Write(memberName); // ClassInfo.MemberNames[0]
w.Write(BinaryTypePrimitiveArray); // MemberTypeInfo.BinaryTypeEnums[0]
w.Write(PrimitiveTypeByte); // MemberTypeInfo.AdditionalInfos[0]
w.Write(libraryId); // LibraryId
// Member value for a PrimitiveArray member: MemberReference pointing at the
// ArraySinglePrimitive record that follows. Inline-array would also be legal
// per §2.7 but MemberReference is the canonical BinaryFormatter shape.
w.Write(RecMemberReference);
w.Write(arrayObjectId);
// ArraySinglePrimitive — §2.4.3.3.
w.Write(RecArraySinglePrimitive);
w.Write(arrayObjectId);
w.Write(data.Length);
w.Write(PrimitiveTypeByte);
w.Write(data);
// MessageEnd — §2.6.3.
w.Write(RecMessageEnd);
return ms.ToArray();
}
///
/// Materialises a frame's ARGB pixels into a top-down BGRA byte[] that matches the
/// shape the decoder returns — for byte-by-byte comparison in the tests.
///
public static byte[] ExtractTopDownBgra(Bitmap frame)
{
var rect = new Rectangle(0, 0, frame.Width, frame.Height);
var locked = frame.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
int rowBytes = frame.Width * 4;
var buffer = new byte[frame.Height * rowBytes];
var row = new byte[Math.Max(locked.Stride, rowBytes)];
for (int y = 0; y < frame.Height; y++)
{
System.Runtime.InteropServices.Marshal.Copy(locked.Scan0 + y * locked.Stride, row, 0, rowBytes);
Buffer.BlockCopy(row, 0, buffer, y * rowBytes, rowBytes);
}
return buffer;
}
finally
{
frame.UnlockBits(locked);
}
}
}