Skip to content

Commit

Permalink
fix Hexa editor display of binary files (#11747)
Browse files Browse the repository at this point in the history
* fix Hexa editor display of binary files

by:
* reading file and blob with a better encoding
* not touching/altering *blob* content (i.e not breaking content by reencoding)
* not converting in ASCII before doing the Hexa editor display (it breaks char > 0x80)

As a result, display is **exactly** the same obtained by using an external Hexa Editor:
* Hexa value are accurate
* string display is the same
(except some cheating done by hexa editor
for example with char 0x99 displayed as ™)

* refactor display binary as hex dump

* DRY
* add display in MB (more human friendly) & translated
  • Loading branch information
pmiossec committed May 24, 2024
1 parent 6cb1e2b commit fbebe3f
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 40 deletions.
7 changes: 4 additions & 3 deletions src/app/GitCommands/Git/GitModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1015,10 +1015,11 @@ public IReadOnlyList<GitRevision> GetParentRevisions(ObjectId objectId)
.ToList();
}

public string? ShowObject(ObjectId objectId)
public string? ShowObject(ObjectId objectId, bool returnRaw)
{
return ReEncodeShowString(_gitExecutable
.GetOutput($"show {objectId}", cache: GitCommandCache, outputEncoding: LosslessEncoding));
string gitOutput = _gitExecutable
.GetOutput($"show {objectId}", cache: GitCommandCache, outputEncoding: LosslessEncoding);
return returnRaw ? gitOutput : ReEncodeShowString(gitOutput);
}

public void DeleteTag(string tagName)
Expand Down
2 changes: 1 addition & 1 deletion src/app/GitExtensions.Extensibility/Git/IGitModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ public interface IGitModule

void DeleteTag(string tagName);

string? ShowObject(ObjectId objectId);
string? ShowObject(ObjectId objectId, bool returnRaw);

IReadOnlyList<GitItemStatus> GetStashDiffFiles(string stashName);

Expand Down
4 changes: 2 additions & 2 deletions src/app/GitUI/CommandsDialogs/FormVerify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ private void Warnings_SelectionChanged(object sender, EventArgs e)

_previewedItem = CurrentItem;

string content = Module.ShowObject(_previewedItem.ObjectId) ?? "";
string content = Module.ShowObject(_previewedItem.ObjectId, returnRaw: _previewedItem.ObjectType == LostObjectType.Blob) ?? "";
if (_previewedItem.ObjectType == LostObjectType.Commit || _previewedItem.ObjectType == LostObjectType.Tag)
{
fileViewer.InvokeAndForget(() => fileViewer.ViewFixedPatchAsync("commit.patch", content, openWithDifftool: null));
Expand Down Expand Up @@ -366,7 +366,7 @@ private void ViewCurrentItem()
return;
}

string? obj = Module.ShowObject(currentItem.ObjectId);
string? obj = Module.ShowObject(currentItem.ObjectId, returnRaw: _previewedItem.ObjectType == LostObjectType.Blob);

if (obj is not null)
{
Expand Down
74 changes: 40 additions & 34 deletions src/app/GitUI/Editor/FileViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public partial class FileViewer : GitModuleControl

private readonly TranslationString _largeFileSizeWarning = new("This file is {0:N1} MB. Showing large files can be slow. Click to show anyway.");
private readonly TranslationString _cannotViewImage = new("Cannot view image {0}");
private readonly TranslationString _fileSizeInMb = new("MB");
private readonly TranslationString _bytes = new("bytes");
private readonly TranslationString _binaryFile = new("Binary file: {0}");
private readonly TranslationString _binaryFileDetected = new("Binary file: {0} (Detected)");

public event EventHandler<SelectedLineEventArgs>? SelectedLineChanged;
public event EventHandler? HScrollPositionChanged;
Expand Down Expand Up @@ -579,21 +583,11 @@ public Task ViewGrepAsync(FileStatusItem item, string text)
{
try
{
StringBuilder summary = new StringBuilder()
.AppendLine("Binary file:")
.AppendLine()
.AppendLine(fileName)
.AppendLine()
.AppendLine($"{text.Length:N0} bytes:")
.AppendLine();
internalFileViewer.SetText(summary.ToString(), openWithDifftool);
ToHexDump(Encoding.ASCII.GetBytes(text), summary);
internalFileViewer.SetText(summary.ToString(), openWithDifftool);
DisplayAsHexDump(_binaryFile.Text, fileName, text, openWithDifftool);
}
catch
{
internalFileViewer.SetText($"Binary file: {fileName} (Detected)", openWithDifftool);
internalFileViewer.SetText(string.Format(_binaryFileDetected.Text, fileName), openWithDifftool);
}
}
else
Expand All @@ -611,6 +605,26 @@ public Task ViewGrepAsync(FileStatusItem item, string text)
});
}

private void DisplayAsHexDump(string fileNameFormat, string filename, string data, Action? openWithDifftool)
{
StringBuilder summary = new StringBuilder()
.AppendLine(string.Format(fileNameFormat, filename))
.AppendLine();

double mb = data.Length / (1024d * 1024);
if (mb >= 0.1)
{
summary.Append($"{mb:N1} {_fileSizeInMb.Text} / ");
}

summary.AppendLine($"{data.Length:N0} {_bytes.Text}:")
.AppendLine();

string hexData = ToHexDump(data, summary);

internalFileViewer.SetText(hexData, openWithDifftool);
}

public Task ViewGitItemAsync(FileStatusItem item, int? line, Action? openWithDifftool)
{
return ViewGitItemAsync(item.Item, item.SecondRevision.ObjectId, item, line, openWithDifftool);
Expand Down Expand Up @@ -671,7 +685,7 @@ private Task ViewGitItemAsync(GitItemStatus file, ObjectId? objectId, FileStatus

string GetFileTextIfBlobExists()
{
FilePreamble = new byte[] { };
FilePreamble = [];
return file.TreeGuid is not null ? Module.GetFileText(file.TreeGuid, Encoding) : string.Empty;
}

Expand Down Expand Up @@ -757,7 +771,7 @@ public Task ViewFileAsync(string fileName, bool isSubmodule = false, FileStatusI
string GetFileText()
{
using FileStream stream = File.Open(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using StreamReader reader = new(stream, Module.FilesEncoding);
using StreamReader reader = new(stream, GitModule.LosslessEncoding);
#pragma warning disable VSTHRD103 // Call async methods when in an async method
string content = reader.ReadToEnd();
#pragma warning restore VSTHRD103 // Call async methods when in an async method
Expand Down Expand Up @@ -1166,17 +1180,9 @@ private Task ViewItemAsync(string fileName, bool isSubmodule, Func<Image?> getIm
{
if (image is null)
{
ResetView(ViewMode.Text, null);
ResetView(ViewMode.Text, fileName, item);
string text = getFileText();
StringBuilder summary = new StringBuilder()
.AppendLine(string.Format(_cannotViewImage.Text, fileName))
.AppendLine()
.AppendLine($"{text.Length:N0} bytes:")
.AppendLine();
ToHexDump(Encoding.ASCII.GetBytes(text), summary);
internalFileViewer.SetText(summary.ToString(), openWithDifftool);
DisplayAsHexDump(_cannotViewImage.Text, fileName, text, openWithDifftool);
return;
}
Expand All @@ -1203,17 +1209,17 @@ private Task ViewItemAsync(string fileName, bool isSubmodule, Func<Image?> getIm
}
}

private static string ToHexDump(byte[] bytes, StringBuilder str, int columnWidth = 8, int columnCount = 2)
private static string ToHexDump(string text, StringBuilder str, int columnWidth = 8, int columnCount = 2)
{
if (bytes.Length == 0)
if (text.Length == 0)
{
return "";
}

// Do not freeze GE when selecting large binary files
// Show only the header of the binary file to indicate contents and files incorrectly handled
// Use a dedicated editor to view the complete file
int limit = Math.Min(bytes.Length, columnWidth * columnCount * 256);
int limit = Math.Min(text.Length, columnWidth * columnCount * 256);
int i = 0;

while (i < limit)
Expand All @@ -1226,7 +1232,7 @@ private static string ToHexDump(byte[] bytes, StringBuilder str, int columnWidth
}

// OFFSET
str.Append($"{baseIndex:X4} ");
str.Append($"{baseIndex:X4} ");

// BYTES
for (int columnIndex = 0; columnIndex < columnCount; columnIndex++)
Expand All @@ -1244,14 +1250,14 @@ private static string ToHexDump(byte[] bytes, StringBuilder str, int columnWidth
str.Append(' ');
}

str.Append(i < bytes.Length
? bytes[i].ToString("X2")
str.Append(i < text.Length
? ((byte)text[i]).ToString("X2")
: " ");
i++;
}
}

str.Append(" ");
str.Append(" ");

// ASCII
i = baseIndex;
Expand All @@ -1265,9 +1271,9 @@ private static string ToHexDump(byte[] bytes, StringBuilder str, int columnWidth

for (int j = 0; j < columnWidth; j++)
{
if (i < bytes.Length)
if (i < text.Length)
{
char c = (char)bytes[i];
char c = text[i];
str.Append(char.IsControl(c) ? '.' : c);
}
else
Expand All @@ -1280,7 +1286,7 @@ private static string ToHexDump(byte[] bytes, StringBuilder str, int columnWidth
}
}

if (bytes.Length > limit)
if (text.Length > limit)
{
str.AppendLine();
str.Append("[Truncated]");
Expand Down
16 changes: 16 additions & 0 deletions src/app/GitUI/Translation/English.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1535,10 +1535,26 @@ The primary difftool can still be selected by clicking the main menu entry.</sou
</file>
<file datatype="plaintext" original="FileViewer" source-language="en">
<body>
<trans-unit id="_binaryFile.Text">
<source>Binary file: {0}</source>
<target />
</trans-unit>
<trans-unit id="_binaryFileDetected.Text">
<source>Binary file: {0} (Detected)</source>
<target />
</trans-unit>
<trans-unit id="_bytes.Text">
<source>bytes</source>
<target />
</trans-unit>
<trans-unit id="_cannotViewImage.Text">
<source>Cannot view image {0}</source>
<target />
</trans-unit>
<trans-unit id="_fileSizeInMb.Text">
<source>MB</source>
<target />
</trans-unit>
<trans-unit id="_largeFileSizeWarning.Text">
<source>This file is {0:N1} MB. Showing large files can be slow. Click to show anyway.</source>
<target />
Expand Down

0 comments on commit fbebe3f

Please sign in to comment.