From e2edea496e2e6820610a99fe816a5fce3179c465 Mon Sep 17 00:00:00 2001 From: Igor Velikorossov Date: Tue, 25 May 2021 12:01:34 +1000 Subject: [PATCH] Allow bypass BinaryFormatter in ImageListStreamer Contributes to #1251 --- .../System/Windows/Forms/ImageListStreamer.cs | 121 +++++++++++------- .../ImageListTests/MauiImageListTests.cs | 30 ++--- .../Windows/Forms/ImageListStreamerTests.cs | 79 ++++++++++++ 3 files changed, 164 insertions(+), 66 deletions(-) create mode 100644 src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageListStreamerTests.cs diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ImageListStreamer.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ImageListStreamer.cs index 325a7fbdd92..21c5ed42edc 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ImageListStreamer.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ImageListStreamer.cs @@ -18,19 +18,16 @@ public sealed class ImageListStreamer : ISerializable, IDisposable // compressed magic header. If we see this, the image stream is compressed. // (unicode for MSFT). private static readonly byte[] HEADER_MAGIC = new byte[] { 0x4D, 0x53, 0x46, 0X74 }; - private static readonly object internalSyncObject = new object(); + private static readonly object s_syncObject = new object(); - private readonly ImageList imageList; - private ImageList.NativeImageList nativeImageList; + private readonly ImageList _imageList; + private ImageList.NativeImageList _nativeImageList; internal ImageListStreamer(ImageList il) { - imageList = il; + _imageList = il; } - /** - * Constructor used in deserialization - */ private ImageListStreamer(SerializationInfo info, StreamingContext context) { SerializationInfoEnumerator sie = info.GetEnumerator(); @@ -47,30 +44,10 @@ private ImageListStreamer(SerializationInfo info, StreamingContext context) try { #endif - byte[] dat = (byte[])sie.Value; - if (dat != null) + byte[] data = (byte[])sie.Value; + if (data is not null) { - // We enclose this imagelist handle create in a theming scope. - IntPtr userCookie = ThemingScope.Activate(Application.UseVisualStyles); - - try - { - using MemoryStream ms = new MemoryStream(Decompress(dat)); - lock (internalSyncObject) - { - ComCtl32.InitCommonControls(); - nativeImageList = new ImageList.NativeImageList(new Ole32.GPStream(ms)); - } - } - finally - { - ThemingScope.Deactivate(userCookie); - } - - if (nativeImageList.Handle == IntPtr.Zero) - { - throw new InvalidOperationException(SR.ImageListStreamerLoadFailed); - } + Deserialize(data); } #if DEBUG } @@ -84,6 +61,23 @@ private ImageListStreamer(SerializationInfo info, StreamingContext context) } } + internal ImageListStreamer(Stream stream) + { + if (stream is MemoryStream ms + && ms.TryGetBuffer(out ArraySegment buffer) + && buffer.Offset == 0) + { + Deserialize(buffer.Array); + } + else + { + stream.Position = 0; + using MemoryStream copyStream = new(checked((int)stream.Length)); + stream.CopyTo(copyStream); + Deserialize(copyStream.GetBuffer()); + } + } + /// /// Compresses the given input, returning a new array that represents /// the compressed data. @@ -206,21 +200,35 @@ private byte[] Decompress(byte[] input) return output; } - public void /*cpr: ISerializable*/GetObjectData(SerializationInfo si, StreamingContext context) + private void Deserialize(byte[] data) { - MemoryStream stream = new MemoryStream(); + // We enclose this imagelist handle create in a theming scope. + IntPtr userCookie = ThemingScope.Activate(Application.UseVisualStyles); - IntPtr handle = IntPtr.Zero; - if (imageList != null) + try + { + using MemoryStream ms = new MemoryStream(Decompress(data)); + lock (s_syncObject) + { + ComCtl32.InitCommonControls(); + _nativeImageList = new ImageList.NativeImageList(new Ole32.GPStream(ms)); + } + } + finally { - handle = imageList.Handle; + ThemingScope.Deactivate(userCookie); } - else if (nativeImageList != null) + + if (_nativeImageList.Handle == IntPtr.Zero) { - handle = nativeImageList.Handle; + throw new InvalidOperationException(SR.ImageListStreamerLoadFailed); } + } - if (handle == IntPtr.Zero || !WriteImageList(handle, stream)) + public void GetObjectData(SerializationInfo si, StreamingContext context) + { + using MemoryStream stream = new MemoryStream(); + if (!WriteImageList(stream)) { throw new InvalidOperationException(SR.ImageListStreamerSaveFailed); } @@ -228,20 +236,43 @@ private byte[] Decompress(byte[] input) si.AddValue("Data", Compress(stream.ToArray())); } + internal void GetObjectData(Stream stream) + { + if (!WriteImageList(stream)) + { + throw new InvalidOperationException(SR.ImageListStreamerSaveFailed); + } + } + internal ImageList.NativeImageList GetNativeImageList() { - return nativeImageList; + return _nativeImageList; } - private bool WriteImageList(IntPtr imagelistHandle, Stream stream) + private bool WriteImageList(Stream stream) { + IntPtr handle = IntPtr.Zero; + if (_imageList != null) + { + handle = _imageList.Handle; + } + else if (_nativeImageList != null) + { + handle = _nativeImageList.Handle; + } + + if (handle == IntPtr.Zero) + { + return false; + } + // What we need to do here is use WriteEx if comctl 6 or above, and Write otherwise. However, till we can fix // There isn't a reliable way to tell which version of comctl fusion is binding to. // So for now, we try to bind to WriteEx, and if that entry point isn't found, we use Write. try { - HRESULT hr = ComCtl32.ImageList.WriteEx(new HandleRef(this, imagelistHandle), ComCtl32.ILP.DOWNLEVEL, new Ole32.GPStream(stream)); + HRESULT hr = ComCtl32.ImageList.WriteEx(new HandleRef(this, handle), ComCtl32.ILP.DOWNLEVEL, new Ole32.GPStream(stream)); return hr == HRESULT.S_OK; } catch (EntryPointNotFoundException) @@ -249,7 +280,7 @@ private bool WriteImageList(IntPtr imagelistHandle, Stream stream) // WriteEx wasn't found - that's fine - we will use Write. } - return ComCtl32.ImageList.Write(new HandleRef(this, imagelistHandle), new Ole32.GPStream(stream)).IsTrue(); + return ComCtl32.ImageList.Write(new HandleRef(this, handle), new Ole32.GPStream(stream)).IsTrue(); } /// @@ -265,10 +296,10 @@ private void Dispose(bool disposing) { if (disposing) { - if (nativeImageList != null) + if (_nativeImageList != null) { - nativeImageList.Dispose(); - nativeImageList = null; + _nativeImageList.Dispose(); + _nativeImageList = null; } } } diff --git a/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs index cb17c100f23..739dcbed64b 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs @@ -11,6 +11,7 @@ using System.Windows.Forms.IntegrationTests.Common; using ReflectTools; using WFCTestLib.Log; +using static System.Windows.Forms.ImageList; using static Interop; namespace System.Windows.Forms.IntegrationTests.MauiTests @@ -85,34 +86,21 @@ private Form CreateForm() form.Text = "ListView Test"; ImageList imageList1 = new(); - imageList1.ImageStream = (ImageListStreamer)FromBase64String(ClassicImageListStreamer); + imageList1.ImageStream = DeserializeStreamer(DevMsImageListStreamer); listView1.SmallImageList = imageList1; return form; } - private static object FromBase64String(string base64String) + private static ImageListStreamer DeserializeStreamer(string base64String) { - byte[] raw = Convert.FromBase64String(base64String); - return FromByteArray(raw); + byte[] bytes = Convert.FromBase64String(base64String); + using MemoryStream ms = new(bytes); + return new ImageListStreamer(ms); } - private static object FromByteArray(byte[] raw) - { - var binaryFormatter = new BinaryFormatter - { - AssemblyFormat = /* FormatterAssemblyStyle.Simple */0 - }; - - using (var serializedStream = new MemoryStream(raw)) - { -#pragma warning disable SYSLIB0011 // Type or member is obsolete - return binaryFormatter.Deserialize(serializedStream); -#pragma warning restore SYSLIB0011 // Type or member is obsolete - } - } - - private const string ClassicImageListStreamer = - "AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAAAqBwAAAk1TRnQBSQFMAwEBAAEIAQABCAEAARABAAEQAQAE/wEJAQAI/wFCAU0BNgEEBgABNgEEAgABKAMAAUADAAEQAwABAQEAAQgGAAEEGAABgAIAAYADAAKAAQABgAMAAYABAAGAAQACgAIAA8ABAAHAAdwBwAEAAfABygGmAQABMwUAATMBAAEzAQABMwEAAjMCAAMWAQADHAEAAyIBAAMpAQADVQEAA00BAANCAQADOQEAAYABfAH/AQACUAH/AQABkwEAAdYBAAH/AewBzAEAAcYB1gHvAQAB1gLnAQABkAGpAa0CAAH/ATMDAAFmAwABmQMAAcwCAAEzAwACMwIAATMBZgIAATMBmQIAATMBzAIAATMB/wIAAWYDAAFmATMCAAJmAgABZgGZAgABZgHMAgABZgH/AgABmQMAAZkBMwIAAZkBZgIAApkCAAGZAcwCAAGZAf8CAAHMAwABzAEzAgABzAFmAgABzAGZAgACzAIAAcwB/wIAAf8BZgIAAf8BmQIAAf8BzAEAATMB/wIAAf8BAAEzAQABMwEAAWYBAAEzAQABmQEAATMBAAHMAQABMwEAAf8BAAH/ATMCAAMzAQACMwFmAQACMwGZAQACMwHMAQACMwH/AQABMwFmAgABMwFmATMBAAEzAmYBAAEzAWYBmQEAATMBZgHMAQABMwFmAf8BAAEzAZkCAAEzAZkBMwEAATMBmQFmAQABMwKZAQABMwGZAcwBAAEzAZkB/wEAATMBzAIAATMBzAEzAQABMwHMAWYBAAEzAcwBmQEAATMCzAEAATMBzAH/AQABMwH/ATMBAAEzAf8BZgEAATMB/wGZAQABMwH/AcwBAAEzAv8BAAFmAwABZgEAATMBAAFmAQABZgEAAWYBAAGZAQABZgEAAcwBAAFmAQAB/wEAAWYBMwIAAWYCMwEAAWYBMwFmAQABZgEzAZkBAAFmATMBzAEAAWYBMwH/AQACZgIAAmYBMwEAA2YBAAJmAZkBAAJmAcwBAAFmAZkCAAFmAZkBMwEAAWYBmQFmAQABZgKZAQABZgGZAcwBAAFmAZkB/wEAAWYBzAIAAWYBzAEzAQABZgHMAZkBAAFmAswBAAFmAcwB/wEAAWYB/wIAAWYB/wEzAQABZgH/AZkBAAFmAf8BzAEAAcwBAAH/AQAB/wEAAcwBAAKZAgABmQEzAZkBAAGZAQABmQEAAZkBAAHMAQABmQMAAZkCMwEAAZkBAAFmAQABmQEzAcwBAAGZAQAB/wEAAZkBZgIAAZkBZgEzAQABmQEzAWYBAAGZAWYBmQEAAZkBZgHMAQABmQEzAf8BAAKZATMBAAKZAWYBAAOZAQACmQHMAQACmQH/AQABmQHMAgABmQHMATMBAAFmAcwBZgEAAZkBzAGZAQABmQLMAQABmQHMAf8BAAGZAf8CAAGZAf8BMwEAAZkBzAFmAQABmQH/AZkBAAGZAf8BzAEAAZkC/wEAAcwDAAGZAQABMwEAAcwBAAFmAQABzAEAAZkBAAHMAQABzAEAAZkBMwIAAcwCMwEAAcwBMwFmAQABzAEzAZkBAAHMATMBzAEAAcwBMwH/AQABzAFmAgABzAFmATMBAAGZAmYBAAHMAWYBmQEAAcwBZgHMAQABmQFmAf8BAAHMAZkCAAHMAZkBMwEAAcwBmQFmAQABzAKZAQABzAGZAcwBAAHMAZkB/wEAAswCAALMATMBAALMAWYBAALMAZkBAAPMAQACzAH/AQABzAH/AgABzAH/ATMBAAGZAf8BZgEAAcwB/wGZAQABzAH/AcwBAAHMAv8BAAHMAQABMwEAAf8BAAFmAQAB/wEAAZkBAAHMATMCAAH/AjMBAAH/ATMBZgEAAf8BMwGZAQAB/wEzAcwBAAH/ATMB/wEAAf8BZgIAAf8BZgEzAQABzAJmAQAB/wFmAZkBAAH/AWYBzAEAAcwBZgH/AQAB/wGZAgAB/wGZATMBAAH/AZkBZgEAAf8CmQEAAf8BmQHMAQAB/wGZAf8BAAH/AcwCAAH/AcwBMwEAAf8BzAFmAQAB/wHMAZkBAAH/AswBAAH/AcwB/wEAAv8BMwEAAcwB/wFmAQAC/wGZAQAC/wHMAQACZgH/AQABZgH/AWYBAAFmAv8BAAH/AmYBAAH/AWYB/wEAAv8BZgEAASEBAAGlAQADXwEAA3cBAAOGAQADlgEAA8sBAAOyAQAD1wEAA90BAAPjAQAD6gEAA/EBAAP4AQAB8AH7Af8BAAGkAqABAAOAAwAB/wIAAf8DAAL/AQAB/wMAAf8BAAH/AQAC/wIAA///AP8A/wD/AAUAAUIBTQE+BwABPgMAASgDAAFAAwABEAMAAQEBAAEBBQABgBcAA/8BAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAL"; + // Base64-reencoded streamer using GetObjectData(Stream) from a decoded ClassicImageListStreamer + private const string DevMsImageListStreamer = + "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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQk0+AAAAAAAAAD4AAAAoAAAAQAAAABAAAAABAAEAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageListStreamerTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageListStreamerTests.cs new file mode 100644 index 00000000000..019a33196fb --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageListStreamerTests.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers.Text; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using Xunit; +using static System.Windows.Forms.ImageList; + +namespace System.Windows.Forms.Tests +{ + public class ImageListStreamerTests : IClassFixture + { + [WinFormsFact] + public void ImageListStreamer_BinaryFormatter_Stream_BinaryFormatter_round_trip_equality() + { + // Create an ImageListStreamer via BinaryFormatter + using ImageListStreamer streamerFromBf = BinarySerialization.EnsureDeserialize(ClassicBfImageListStreamer); + using NativeImageList nativeImageListBf = streamerFromBf.GetNativeImageList(); + Assert.NotEqual(IntPtr.Zero, nativeImageListBf.Handle); + + // Read as a memory stream + using MemoryStream ms = new(); + streamerFromBf.GetObjectData(ms); + string msBase64 = Convert.ToBase64String(ms.ToArray()); + + // Create a new ImageListStreamer from the stream + ms.Position = 0; + using ImageListStreamer streamerFromMs = new(ms); + using NativeImageList nativeImageListMs = streamerFromMs.GetNativeImageList(); + Assert.NotEqual(IntPtr.Zero, nativeImageListMs.Handle); + + // Compare the two + using ImageList imageListBf = new(); + imageListBf.ImageStream = streamerFromBf; + using ImageList imageListMs = new(); + imageListMs.ImageStream = streamerFromMs; + + Assert.Equal(imageListBf.ColorDepth, imageListMs.ColorDepth); + Assert.Equal(imageListBf.Images.Count, imageListMs.Images.Count); + Assert.Equal(imageListBf.ImageSize, imageListMs.ImageSize); + } + + [WinFormsFact] + public void ImageListStreamer_Stream_BinaryFormatter_compatible() + { + // Create a new ImageListStreamer from the stream + byte[] bytes = Convert.FromBase64String(DevMsImageListStreamer); + using MemoryStream ms = new(bytes); + using ImageListStreamer streamerFromMs = new(ms); + using NativeImageList nativeImageListMs = streamerFromMs.GetNativeImageList(); + Assert.NotEqual(IntPtr.Zero, nativeImageListMs.Handle); + + // Create an ImageListStreamer via BinaryFormatter + using ImageListStreamer streamerFromBf = BinarySerialization.EnsureDeserialize(ClassicBfImageListStreamer); + using NativeImageList nativeImageListBf = streamerFromBf.GetNativeImageList(); + Assert.NotEqual(IntPtr.Zero, nativeImageListBf.Handle); + + // Compare the two + using ImageList imageListBf = new(); + imageListBf.ImageStream = streamerFromBf; + using ImageList imageListMs = new(); + imageListMs.ImageStream = streamerFromMs; + + Assert.Equal(imageListBf.ColorDepth, imageListMs.ColorDepth); + Assert.Equal(imageListBf.Images.Count, imageListMs.Images.Count); + Assert.Equal(imageListBf.ImageSize, imageListMs.ImageSize); + } + + private const string ClassicBfImageListStreamer = + "AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAAAqBwAAAk1TRnQBSQFMAwEBAAEIAQABCAEAARABAAEQAQAE/wEJAQAI/wFCAU0BNgEEBgABNgEEAgABKAMAAUADAAEQAwABAQEAAQgGAAEEGAABgAIAAYADAAKAAQABgAMAAYABAAGAAQACgAIAA8ABAAHAAdwBwAEAAfABygGmAQABMwUAATMBAAEzAQABMwEAAjMCAAMWAQADHAEAAyIBAAMpAQADVQEAA00BAANCAQADOQEAAYABfAH/AQACUAH/AQABkwEAAdYBAAH/AewBzAEAAcYB1gHvAQAB1gLnAQABkAGpAa0CAAH/ATMDAAFmAwABmQMAAcwCAAEzAwACMwIAATMBZgIAATMBmQIAATMBzAIAATMB/wIAAWYDAAFmATMCAAJmAgABZgGZAgABZgHMAgABZgH/AgABmQMAAZkBMwIAAZkBZgIAApkCAAGZAcwCAAGZAf8CAAHMAwABzAEzAgABzAFmAgABzAGZAgACzAIAAcwB/wIAAf8BZgIAAf8BmQIAAf8BzAEAATMB/wIAAf8BAAEzAQABMwEAAWYBAAEzAQABmQEAATMBAAHMAQABMwEAAf8BAAH/ATMCAAMzAQACMwFmAQACMwGZAQACMwHMAQACMwH/AQABMwFmAgABMwFmATMBAAEzAmYBAAEzAWYBmQEAATMBZgHMAQABMwFmAf8BAAEzAZkCAAEzAZkBMwEAATMBmQFmAQABMwKZAQABMwGZAcwBAAEzAZkB/wEAATMBzAIAATMBzAEzAQABMwHMAWYBAAEzAcwBmQEAATMCzAEAATMBzAH/AQABMwH/ATMBAAEzAf8BZgEAATMB/wGZAQABMwH/AcwBAAEzAv8BAAFmAwABZgEAATMBAAFmAQABZgEAAWYBAAGZAQABZgEAAcwBAAFmAQAB/wEAAWYBMwIAAWYCMwEAAWYBMwFmAQABZgEzAZkBAAFmATMBzAEAAWYBMwH/AQACZgIAAmYBMwEAA2YBAAJmAZkBAAJmAcwBAAFmAZkCAAFmAZkBMwEAAWYBmQFmAQABZgKZAQABZgGZAcwBAAFmAZkB/wEAAWYBzAIAAWYBzAEzAQABZgHMAZkBAAFmAswBAAFmAcwB/wEAAWYB/wIAAWYB/wEzAQABZgH/AZkBAAFmAf8BzAEAAcwBAAH/AQAB/wEAAcwBAAKZAgABmQEzAZkBAAGZAQABmQEAAZkBAAHMAQABmQMAAZkCMwEAAZkBAAFmAQABmQEzAcwBAAGZAQAB/wEAAZkBZgIAAZkBZgEzAQABmQEzAWYBAAGZAWYBmQEAAZkBZgHMAQABmQEzAf8BAAKZATMBAAKZAWYBAAOZAQACmQHMAQACmQH/AQABmQHMAgABmQHMATMBAAFmAcwBZgEAAZkBzAGZAQABmQLMAQABmQHMAf8BAAGZAf8CAAGZAf8BMwEAAZkBzAFmAQABmQH/AZkBAAGZAf8BzAEAAZkC/wEAAcwDAAGZAQABMwEAAcwBAAFmAQABzAEAAZkBAAHMAQABzAEAAZkBMwIAAcwCMwEAAcwBMwFmAQABzAEzAZkBAAHMATMBzAEAAcwBMwH/AQABzAFmAgABzAFmATMBAAGZAmYBAAHMAWYBmQEAAcwBZgHMAQABmQFmAf8BAAHMAZkCAAHMAZkBMwEAAcwBmQFmAQABzAKZAQABzAGZAcwBAAHMAZkB/wEAAswCAALMATMBAALMAWYBAALMAZkBAAPMAQACzAH/AQABzAH/AgABzAH/ATMBAAGZAf8BZgEAAcwB/wGZAQABzAH/AcwBAAHMAv8BAAHMAQABMwEAAf8BAAFmAQAB/wEAAZkBAAHMATMCAAH/AjMBAAH/ATMBZgEAAf8BMwGZAQAB/wEzAcwBAAH/ATMB/wEAAf8BZgIAAf8BZgEzAQABzAJmAQAB/wFmAZkBAAH/AWYBzAEAAcwBZgH/AQAB/wGZAgAB/wGZATMBAAH/AZkBZgEAAf8CmQEAAf8BmQHMAQAB/wGZAf8BAAH/AcwCAAH/AcwBMwEAAf8BzAFmAQAB/wHMAZkBAAH/AswBAAH/AcwB/wEAAv8BMwEAAcwB/wFmAQAC/wGZAQAC/wHMAQACZgH/AQABZgH/AWYBAAFmAv8BAAH/AmYBAAH/AWYB/wEAAv8BZgEAASEBAAGlAQADXwEAA3cBAAOGAQADlgEAA8sBAAOyAQAD1wEAA90BAAPjAQAD6gEAA/EBAAP4AQAB8AH7Af8BAAGkAqABAAOAAwAB/wIAAf8DAAL/AQAB/wMAAf8BAAH/AQAC/wIAA///AP8A/wD/AAUAAUIBTQE+BwABPgMAASgDAAFAAwABEAMAAQEBAAEBBQABgBcAA/8BAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAL"; + + // Base64-reencoded streamer using GetObjectData(Stream) from a decoded ClassicBfImageListStreamer + private const string DevMsImageListStreamer = + "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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQk0+AAAAAAAAAD4AAAAoAAAAQAAAABAAAAABAAEAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + } +}