diff --git a/src/Microsoft.DotNet.Interactive.Formatting.Tests/BinaryFormatterTests.cs b/src/Microsoft.DotNet.Interactive.Formatting.Tests/BinaryFormatterTests.cs new file mode 100644 index 0000000000..bf205b8003 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.Formatting.Tests/BinaryFormatterTests.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using FluentAssertions; +using Microsoft.DotNet.Interactive.Formatting.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.Formatting.Tests; + +public class BinaryFormatterTests : FormatterTestBase +{ + [Fact] + public void Byte_array_formats_as_hex_dump_in_plain_text() + { + var bytes = new byte[] { + 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, // "Hello World!" + 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21 + }; + + var formatted = bytes.ToDisplayString(PlainTextFormatter.MimeType); + + formatted.Should().Contain("00000000"); + formatted.Should().Contain("48 65 6C 6C 6F 20 57 6F"); + formatted.Should().Contain("|Hello World!"); + } + + [Fact] + public void Byte_array_formats_as_hex_dump_in_html() + { + var bytes = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F }; + + var formatted = bytes.ToDisplayString(HtmlFormatter.MimeType).RemoveStyleElement(); + + formatted.Should().Contain("
");
+        formatted.Should().Contain("00000000");
+        formatted.Should().Contain("48 65 6C 6C 6F");
+        formatted.Should().Contain("
"); + } + + [Fact] + public void Empty_byte_array_produces_empty_output() + { + var bytes = Array.Empty(); + + var formatted = bytes.ToDisplayString(PlainTextFormatter.MimeType); + + formatted.Should().BeEmpty(); + } + + [Fact] + public void Null_byte_array_shows_null_string() + { + byte[] bytes = null; + + var formatted = bytes.ToDisplayString(PlainTextFormatter.MimeType); + + formatted.Should().Contain(Formatter.NullString); + } + + [Fact] + public void Long_byte_array_spans_multiple_lines() + { + var bytes = new byte[32]; // Two lines worth + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = (byte)i; + } + + var formatted = bytes.ToDisplayString(PlainTextFormatter.MimeType); + + formatted.Should().Contain("00000000"); + formatted.Should().Contain("00000010"); + } + + [Fact] + public void Non_printable_characters_show_as_dots() + { + var bytes = new byte[] { 0x00, 0x01, 0x02, 0xFF }; + + var formatted = bytes.ToDisplayString(PlainTextFormatter.MimeType); + + formatted.Should().Contain("|....|"); + } + + [Fact] + public void ReadOnlyMemory_byte_formats_like_byte_array() + { + var bytes = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F }; + var memory = new ReadOnlyMemory(bytes); + + var formatted = memory.ToDisplayString(PlainTextFormatter.MimeType); + + formatted.Should().Contain("00000000"); + formatted.Should().Contain("48 65 6C 6C 6F"); + } + + [Fact] + public void Partial_last_line_is_padded_correctly() + { + var bytes = new byte[] { 0x01, 0x02, 0x03, 0x04 }; // Only 4 bytes + + var formatted = bytes.ToDisplayString(PlainTextFormatter.MimeType); + + // Should have proper spacing even with fewer than 16 bytes + formatted.Should().Contain("01 02 03 04"); + formatted.Should().Contain("|....|"); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.Formatting/BinaryFormatter.cs b/src/Microsoft.DotNet.Interactive.Formatting/BinaryFormatter.cs new file mode 100644 index 0000000000..9e5e9734e8 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.Formatting/BinaryFormatter.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.Interactive.Formatting; + +/// +/// Provides formatting for binary data (byte arrays) with hexadecimal representation. +/// +public static class BinaryFormatter +{ + private const int BytesPerLine = 16; + + public static string FormatBytes(byte[] bytes) + { + if (bytes is null || bytes.Length == 0) + { + return string.Empty; + } + + using var writer = new StringWriter(); + FormatBytesTo(bytes, writer); + return writer.ToString(); + } + + public static void FormatBytesTo(byte[] bytes, TextWriter writer) + { + if (bytes is null || bytes.Length == 0) + { + return; + } + + for (int offset = 0; offset < bytes.Length; offset += BytesPerLine) + { + // Write address offset + writer.Write($"{offset:X8} "); + + // Write hex values + int bytesInLine = Math.Min(BytesPerLine, bytes.Length - offset); + + for (int i = 0; i < BytesPerLine; i++) + { + if (i < bytesInLine) + { + writer.Write($"{bytes[offset + i]:X2} "); + } + else + { + writer.Write(" "); + } + + // Add extra space after 8 bytes for readability + if (i == 7) + { + writer.Write(" "); + } + } + + // Write ASCII representation + writer.Write(" |"); + for (int i = 0; i < bytesInLine; i++) + { + byte b = bytes[offset + i]; + char c = (b >= 32 && b < 127) ? (char)b : '.'; + writer.Write(c); + } + writer.Write("|"); + + if (offset + BytesPerLine < bytes.Length) + { + writer.WriteLine(); + } + } + } + + internal static ITypeFormatter[] DefaultFormatters { get; } = + { + // PlainText formatter for byte arrays + new PlainTextFormatter((bytes, context) => + { + if (bytes is null) + { + context.Writer.Write(Formatter.NullString); + return true; + } + + FormatBytesTo(bytes, context.Writer); + return true; + }), + + // HTML formatter for byte arrays + new HtmlFormatter((bytes, context) => + { + if (bytes is null) + { + context.Writer.Write(Formatter.NullString); + return true; + } + + context.Writer.Write("
");
+            FormatBytesTo(bytes, context.Writer);
+            context.Writer.Write("
"); + return true; + }), + + // PlainText formatter for ReadOnlyMemory + new AnonymousTypeFormatter( + type: typeof(ReadOnlyMemory), + mimeType: PlainTextFormatter.MimeType, + format: (value, context) => + { + var readOnlyMemory = (ReadOnlyMemory)value; + var bytes = readOnlyMemory.ToArray(); + FormatBytesTo(bytes, context.Writer); + return true; + }), + + // HTML formatter for ReadOnlyMemory + new AnonymousTypeFormatter( + type: typeof(ReadOnlyMemory), + mimeType: HtmlFormatter.MimeType, + format: (value, context) => + { + var readOnlyMemory = (ReadOnlyMemory)value; + var bytes = readOnlyMemory.ToArray(); + + context.Writer.Write("
");
+                FormatBytesTo(bytes, context.Writer);
+                context.Writer.Write("
"); + return true; + }) + }; +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.Formatting/Formatter.cs b/src/Microsoft.DotNet.Interactive.Formatting/Formatter.cs index 5f48c3a155..772a3c604e 100644 --- a/src/Microsoft.DotNet.Interactive.Formatting/Formatter.cs +++ b/src/Microsoft.DotNet.Interactive.Formatting/Formatter.cs @@ -117,6 +117,7 @@ public static void ResetToDefault() _defaultTypeFormatters.PushRange(((IEnumerable)JsonFormatter.DefaultFormatters).Reverse().ToArray()); _defaultTypeFormatters.PushRange(((IEnumerable)PlainTextSummaryFormatter.DefaultFormatters).Reverse().ToArray()); _defaultTypeFormatters.PushRange(((IEnumerable)PlainTextFormatter.DefaultFormatters).Reverse().ToArray()); + _defaultTypeFormatters.PushRange(((IEnumerable)BinaryFormatter.DefaultFormatters).Reverse().ToArray()); _defaultPreferredMimeTypes.Push((typeof(string), PlainTextFormatter.MimeType));