Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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("<pre class=\"dni-binary\">");
formatted.Should().Contain("00000000");
formatted.Should().Contain("48 65 6C 6C 6F");
formatted.Should().Contain("</pre>");
}

[Fact]
public void Empty_byte_array_produces_empty_output()
{
var bytes = Array.Empty<byte>();

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<byte>(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("|....|");
}
}
135 changes: 135 additions & 0 deletions src/Microsoft.DotNet.Interactive.Formatting/BinaryFormatter.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides formatting for binary data (byte arrays) with hexadecimal representation.
/// </summary>
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<byte[]>((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<byte[]>((bytes, context) =>
{
if (bytes is null)
{
context.Writer.Write(Formatter.NullString);
return true;
}

context.Writer.Write("<pre class=\"dni-binary\">");
FormatBytesTo(bytes, context.Writer);
context.Writer.Write("</pre>");
return true;
}),

// PlainText formatter for ReadOnlyMemory<byte>
new AnonymousTypeFormatter<object>(
type: typeof(ReadOnlyMemory<byte>),
mimeType: PlainTextFormatter.MimeType,
format: (value, context) =>
{
var readOnlyMemory = (ReadOnlyMemory<byte>)value;
var bytes = readOnlyMemory.ToArray();
FormatBytesTo(bytes, context.Writer);
return true;
}),

// HTML formatter for ReadOnlyMemory<byte>
new AnonymousTypeFormatter<object>(
type: typeof(ReadOnlyMemory<byte>),
mimeType: HtmlFormatter.MimeType,
format: (value, context) =>
{
var readOnlyMemory = (ReadOnlyMemory<byte>)value;
var bytes = readOnlyMemory.ToArray();

context.Writer.Write("<pre class=\"dni-binary\">");
FormatBytesTo(bytes, context.Writer);
context.Writer.Write("</pre>");
return true;
})
};
}
1 change: 1 addition & 0 deletions src/Microsoft.DotNet.Interactive.Formatting/Formatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public static void ResetToDefault()
_defaultTypeFormatters.PushRange(((IEnumerable<ITypeFormatter>)JsonFormatter.DefaultFormatters).Reverse().ToArray());
_defaultTypeFormatters.PushRange(((IEnumerable<ITypeFormatter>)PlainTextSummaryFormatter.DefaultFormatters).Reverse().ToArray());
_defaultTypeFormatters.PushRange(((IEnumerable<ITypeFormatter>)PlainTextFormatter.DefaultFormatters).Reverse().ToArray());
_defaultTypeFormatters.PushRange(((IEnumerable<ITypeFormatter>)BinaryFormatter.DefaultFormatters).Reverse().ToArray());

_defaultPreferredMimeTypes.Push((typeof(string), PlainTextFormatter.MimeType));

Expand Down
Loading