/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using VST = Microsoft.VisualStudio.Text; namespace dnSpy.Contracts.Hex { /// /// Line information /// public abstract class HexBufferLine { /// /// Constructor /// protected HexBufferLine() { } /// /// Gets the instance that created this line /// public abstract HexBufferLineFormatter LineProvider { get; } /// /// Gets the buffer /// public HexBuffer Buffer => LineProvider.Buffer; /// /// Gets the line number /// public abstract HexPosition LineNumber { get; } /// /// Gets the column order /// public abstract ReadOnlyCollection ColumnOrder { get; } /// /// Buffer span /// public abstract HexBufferSpan BufferSpan { get; } /// /// Gets the start position /// public HexBufferPoint BufferStart => BufferSpan.Start; /// /// Gets the end position /// public HexBufferPoint BufferEnd => BufferSpan.End; /// /// All raw bytes /// public abstract HexBytes HexBytes { get; } /// /// Text shown in the UI. The positions of the offset column, values column /// and ASCII column are not fixed, use one of the GetXXX methods to get /// the spans. /// public abstract string Text { get; } /// /// Gets a span covering /// public VST.Span TextSpan => new VST.Span(0, Text.Length); /// /// true if the offset column is present /// public abstract bool IsOffsetColumnPresent { get; } /// /// true if the values column is present /// public abstract bool IsValuesColumnPresent { get; } /// /// true if the ASCII column is present /// public abstract bool IsAsciiColumnPresent { get; } /// /// Returns true if a column is present /// /// Column /// public bool IsColumnPresent(HexColumnType column) { switch (column) { case HexColumnType.Offset: return IsOffsetColumnPresent; case HexColumnType.Values: return IsValuesColumnPresent; case HexColumnType.Ascii: return IsAsciiColumnPresent; default: throw new ArgumentOutOfRangeException(nameof(column)); } } /// /// Gets the value of the offset shown in . The real offset /// is stored in /// public abstract HexPosition LogicalOffset { get; } /// /// Gets the span of a column /// /// Colum /// true to only include visible values, false to include the full column /// public VST.Span GetSpan(HexColumnType column, bool onlyVisibleCells) { switch (column) { case HexColumnType.Offset: return GetOffsetSpan(); case HexColumnType.Values: return GetValuesSpan(onlyVisibleCells); case HexColumnType.Ascii: return GetAsciiSpan(onlyVisibleCells); default: throw new ArgumentOutOfRangeException(nameof(column)); } } /// /// Gets the span of the offset in . This can be an empty span if /// the offset column isn't shown. /// /// public abstract VST.Span GetOffsetSpan(); /// /// Gets the span of the values column /// /// true to only include visible values, false to include the full column /// public abstract VST.Span GetValuesSpan(bool onlyVisibleCells); /// /// Gets values spans /// /// Span and selection flags /// public IEnumerable GetValuesSpans(HexBufferSpanSelection span) => GetValuesSpans(span.BufferSpan, span.SelectionFlags); /// /// Gets values spans /// /// Buffer span /// Flags /// public IEnumerable GetValuesSpans(HexBufferSpan span, HexSpanSelectionFlags flags) => GetTextAndHexSpans(IsValuesColumnPresent, ValueCells, span, flags, GetValuesSpan(onlyVisibleCells: true), GetValuesSpan(onlyVisibleCells: false)); /// /// Gets the span of the ASCII column. This can be an empty span /// if the ASCII column isn't shown. /// /// true to only include visible characters, false to include the full column /// public abstract VST.Span GetAsciiSpan(bool onlyVisibleCells); /// /// Gets ASCII spans /// /// Span and selection flags /// public IEnumerable GetAsciiSpans(HexBufferSpanSelection span) => GetAsciiSpans(span.BufferSpan, span.SelectionFlags); /// /// Gets ASCII spans /// /// Buffer span /// Flags /// public IEnumerable GetAsciiSpans(HexBufferSpan span, HexSpanSelectionFlags flags) => GetTextAndHexSpans(IsAsciiColumnPresent, AsciiCells, span, flags, GetAsciiSpan(onlyVisibleCells: true), GetAsciiSpan(onlyVisibleCells: false)); /// /// Gets column spans in column order /// /// Span and selection flags /// public IEnumerable GetSpans(HexBufferSpanSelection span) => GetSpans(span.BufferSpan, span.SelectionFlags); /// /// Gets column spans in column order /// /// Buffer span /// Flags /// public IEnumerable GetSpans(HexBufferSpan span, HexSpanSelectionFlags flags) { if (span.IsDefault) throw new ArgumentException(); if (span.Buffer != Buffer) throw new ArgumentException(); var overlapSpan = BufferSpan.Overlap(span); if (overlapSpan is null) yield break; foreach (var column in ColumnOrder) { switch (column) { case HexColumnType.Offset: if ((flags & HexSpanSelectionFlags.Offset) != 0 && IsOffsetColumnPresent) { if (BufferSpan.Contains(overlapSpan.Value)) yield return new TextAndHexSpan(GetOffsetSpan(), BufferSpan); } break; case HexColumnType.Values: if ((flags & HexSpanSelectionFlags.Values) != 0) { foreach (var info in GetValuesSpans(overlapSpan.Value, flags)) yield return info; } break; case HexColumnType.Ascii: if ((flags & HexSpanSelectionFlags.Ascii) != 0) { foreach (var info in GetAsciiSpans(overlapSpan.Value, flags)) yield return info; } break; default: throw new InvalidOperationException(); } } } /// /// Gets the value cell collection /// public abstract HexCellCollection ValueCells { get; } /// /// Gets the ASCII cell collection /// public abstract HexCellCollection AsciiCells { get; } TextAndHexSpan Create(HexCellCollection collection, HexCell first, HexCell last, HexBufferSpan bufferSpan) { var firstCellSpan = first.FullSpan; var lastCellSpan = last.FullSpan; var startPos = HexPosition.MaxEndPosition; var endPos = HexPosition.Zero; for (int i = first.Index; i <= last.Index; i++) { var cell = collection[i]; if (!cell.HasData) continue; startPos = HexPosition.Min(startPos, cell.BufferStart); endPos = HexPosition.Max(endPos, cell.BufferEnd); } var resultBufferSpan = startPos <= endPos ? new HexBufferSpan(new HexBufferPoint(Buffer, startPos), new HexBufferPoint(Buffer, endPos)) : bufferSpan; return new TextAndHexSpan(VST.Span.FromBounds(firstCellSpan.Start, lastCellSpan.End), resultBufferSpan); } IEnumerable GetTextAndHexSpans(bool isColumnPresent, HexCellCollection collection, HexBufferSpan span, HexSpanSelectionFlags flags, VST.Span visibleSpan, VST.Span fullSpan) { if (span.IsDefault) throw new ArgumentException(); if (span.Buffer != Buffer) throw new ArgumentException(); if (!isColumnPresent) yield break; var overlapSpan = BufferSpan.Overlap(span); if (overlapSpan is null) yield break; if ((flags & (HexSpanSelectionFlags.Group0 | HexSpanSelectionFlags.Group1)) != 0) { bool group0 = (flags & HexSpanSelectionFlags.Group0) != 0; bool group1 = (flags & HexSpanSelectionFlags.Group1) != 0; IEnumerable cells; if ((flags & HexSpanSelectionFlags.AllCells) != 0) { cells = collection.GetCells(); overlapSpan = BufferSpan; } else if ((flags & HexSpanSelectionFlags.AllVisibleCells) != 0) { cells = collection.GetVisibleCells(); overlapSpan = BufferSpan; } else cells = collection.GetCells(overlapSpan.Value); HexCell? firstCell = null; HexCell? lastCell = null; foreach (var cell in cells) { if (!((cell.GroupIndex == 0 && group0) || (cell.GroupIndex == 1 && group1))) continue; if (firstCell is null) { firstCell = cell; lastCell = cell; } else if (lastCell!.Index + 1 == cell.Index && lastCell.GroupIndex == cell.GroupIndex) lastCell = cell; else { yield return Create(collection, firstCell, lastCell, overlapSpan.Value); firstCell = lastCell = cell; } } if (firstCell is not null) yield return Create(collection, firstCell, lastCell!, overlapSpan.Value); yield break; } if ((flags & HexSpanSelectionFlags.AllVisibleCells) != 0) { yield return new TextAndHexSpan(visibleSpan, BufferSpan); yield break; } if ((flags & HexSpanSelectionFlags.AllCells) != 0) { yield return new TextAndHexSpan(fullSpan, BufferSpan); yield break; } if ((flags & HexSpanSelectionFlags.OneValue) != 0) { foreach (var cell in collection.GetCells(overlapSpan.Value)) { if (!cell.HasData) continue; var cellSpan = cell.GetSpan(flags); yield return new TextAndHexSpan(cellSpan, new HexBufferSpan(Buffer, cell.BufferSpan)); } } else { int textStart = int.MaxValue; int textEnd = int.MinValue; var posStart = HexPosition.MaxValue; var posEnd = HexPosition.MinValue; foreach (var cell in collection.GetCells(overlapSpan.Value)) { if (!cell.HasData) continue; var cellSpan = cell.GetSpan(flags); textStart = Math.Min(textStart, cellSpan.Start); textEnd = Math.Max(textEnd, cellSpan.End); posStart = HexPosition.Min(posStart, cell.BufferStart); posEnd = HexPosition.Max(posEnd, cell.BufferEnd); } if (textStart > textEnd || posStart > posEnd) yield break; yield return new TextAndHexSpan(VST.Span.FromBounds(textStart, textEnd), new HexBufferSpan(Buffer, HexSpan.FromBounds(posStart, posEnd))); } } /// /// Gets a text line position or null /// /// Position /// public int? GetLinePosition(HexCellPosition position) { if (position.IsDefault) throw new ArgumentException(); HexCellCollection collection; switch (position.Column) { case HexColumnType.Values: collection = ValueCells; break; case HexColumnType.Ascii: collection = AsciiCells; break; case HexColumnType.Offset: default: throw new ArgumentOutOfRangeException(nameof(position)); } var cell = collection.GetCell(position.BufferPosition); if (cell is null) return null; if (position.CellPosition >= cell.CellSpan.Length) return null; return cell.CellSpan.Start + position.CellPosition; } /// /// Creates a /// /// Line position /// public HexLinePositionInfo GetLinePositionInfo(int linePosition) { if (linePosition >= Text.Length) return HexLinePositionInfo.CreateVirtualSpace(linePosition, Text.Length); if (IsOffsetColumnPresent) { var span = GetOffsetSpan(); if (span.Contains(linePosition)) return HexLinePositionInfo.CreateOffset(linePosition, linePosition - span.Start); } if (IsValuesColumnPresent) { var valuesSpan = GetValuesSpan(onlyVisibleCells: false); if (valuesSpan.Contains(linePosition)) { int cellIndex = (linePosition - valuesSpan.Start) / LineProvider.GetCharsPerCellIncludingSeparator(HexColumnType.Values); var cell = ValueCells[cellIndex]; if (cell.SeparatorSpan.Contains(linePosition)) return HexLinePositionInfo.CreateValueCellSeparator(linePosition, cell); return HexLinePositionInfo.CreateValue(linePosition, cell); } } if (IsAsciiColumnPresent) { var asciiSpan = GetAsciiSpan(onlyVisibleCells: false); if (asciiSpan.Contains(linePosition)) { int cellIndex = linePosition - asciiSpan.Start; var cell = AsciiCells[cellIndex]; return HexLinePositionInfo.CreateAscii(linePosition, cell); } } foreach (var column in ColumnOrder) { var span = GetSpan(column, onlyVisibleCells: false); if (span.End == linePosition) return HexLinePositionInfo.CreateColumnSeparator(linePosition); } throw new InvalidOperationException(); } /// /// Gets the closest cell position /// /// Line position /// true to only return cells with data /// public HexCellPosition? GetClosestCellPosition(int linePosition, bool onlyVisibleCells) { var posInfo = GetLinePositionInfo(linePosition); return GetClosestCellPosition(posInfo, onlyVisibleCells); } /// /// Gets the closest cell position /// /// Position /// true to only return cells with data /// public HexCellPosition? GetClosestCellPosition(HexLinePositionInfo position, bool onlyVisibleCells) { switch (position.Type) { case HexLinePositionInfoType.ValueCell: case HexLinePositionInfoType.AsciiCell: break; case HexLinePositionInfoType.ValueCellSeparator: Debug2.Assert(position.Cell is not null); Debug.Assert(position.Cell.CellSpan.End == position.Position); position = HexLinePositionInfo.CreateValue(position.Cell.CellSpan.End - 1, position.Cell); break; case HexLinePositionInfoType.Offset: case HexLinePositionInfoType.ColumnSeparator: case HexLinePositionInfoType.VirtualSpace: var closestPos = GetClosestCellPosition(position.Position); if (closestPos is null) return null; Debug.Assert(closestPos.Value.IsAsciiCell || closestPos.Value.IsValueCell); position = closestPos.Value; break; default: throw new InvalidOperationException(); } var cell = position.Cell; int cellPosition = position.CellPosition; switch (position.Type) { case HexLinePositionInfoType.AsciiCell: if (!IsAsciiColumnPresent) return null; Debug2.Assert(cell is not null); if (onlyVisibleCells && !cell.HasData) { var visible = GetVisible(AsciiCells, cell); if (visible is null) return null; cell = visible.Value.cell; cellPosition = visible.Value.cellPosition; } return new HexCellPosition(HexColumnType.Ascii, cell.BufferStart, cellPosition); case HexLinePositionInfoType.ValueCell: Debug2.Assert(cell is not null); if (!IsValuesColumnPresent) return null; if (onlyVisibleCells && !cell.HasData) { var visible = GetVisible(ValueCells, cell); if (visible is null) return null; cell = visible.Value.cell; cellPosition = visible.Value.cellPosition; } return new HexCellPosition(HexColumnType.Values, LineProvider.GetValueBufferSpan(cell, cellPosition).Start, cellPosition); case HexLinePositionInfoType.Offset: case HexLinePositionInfoType.ValueCellSeparator: case HexLinePositionInfoType.ColumnSeparator: case HexLinePositionInfoType.VirtualSpace: default: throw new InvalidOperationException(); } } static (HexCell cell, int cellPosition)? GetVisible(HexCellCollection collection, HexCell cell) { if (cell.HasData) throw new ArgumentException(); for (int i = cell.Index + 1; i < collection.Count; i++) { var c = collection[i]; if (!c.HasData) continue; return (c, 0); } for (int i = cell.Index - 1; i >= 0; i--) { var c = collection[i]; if (!c.HasData) continue; return (c, c.CellSpan.Length - 1); } return null; } HexLinePositionInfo? GetClosestCellPosition(int linePosition) { (HexColumnType columnType, HexCell cell)? closest = null; int cellPosition = -1; foreach (var info in GetCells()) { var cell = info.cell; if (closest is null || Compare(linePosition, cell, closest.Value.cell) < 0) { closest = info; cellPosition = linePosition - info.cell.CellSpan.Start; if (cellPosition < 0) cellPosition = 0; else if (cellPosition >= info.cell.CellSpan.Length) cellPosition = info.cell.CellSpan.Length - 1; } } if (closest is null) return null; if (cellPosition < 0 || cellPosition >= closest.Value.cell.CellSpan.Length) throw new InvalidOperationException(); int pos = closest.Value.cell.CellSpan.Start + cellPosition; if (closest.Value.columnType == HexColumnType.Values) return HexLinePositionInfo.CreateValue(pos, closest.Value.cell); if (closest.Value.columnType == HexColumnType.Ascii) return HexLinePositionInfo.CreateAscii(pos, closest.Value.cell); throw new InvalidOperationException(); } static int Compare(int linePosition, HexCell a, HexCell b) { int da = GetLength(linePosition, a); int db = GetLength(linePosition, b); return da - db; } static int GetLength(int linePosition, HexCell a) { int sl = Math.Abs(linePosition - a.FullSpan.Start); int el = Math.Abs(linePosition - (a.FullSpan.End - 1)); return Math.Min(sl, el); } IEnumerable<(HexColumnType columnType, HexCell cell)> GetCells() { foreach (var column in ColumnOrder) { switch (column) { case HexColumnType.Offset: break; case HexColumnType.Values: if (IsValuesColumnPresent) { foreach (var cell in ValueCells.GetCells()) yield return (HexColumnType.Values, cell); } break; case HexColumnType.Ascii: if (IsAsciiColumnPresent) { foreach (var cell in AsciiCells.GetCells()) yield return (HexColumnType.Ascii, cell); } break; default: throw new InvalidOperationException(); } } } /// /// Gets the text. All cells outside the input range are cleared. /// /// Visible bytes /// public string GetText(HexBufferSpan visibleBytes) { if (visibleBytes.Contains(BufferSpan)) return Text; var chars = Text.ToCharArray(); ClearCells(chars, IsValuesColumnPresent, ValueCells, visibleBytes); ClearCells(chars, IsAsciiColumnPresent, AsciiCells, visibleBytes); return new string(chars); } void ClearCells(char[] chars, bool isColumnPresent, HexCellCollection cells, HexBufferSpan visibleBytes) { if (!isColumnPresent) return; foreach (var cell in cells.GetVisibleCells()) { // Don't clear it if the cell is in the visible bytes span, this includes the case // where only some of its bytes are visible. if (visibleBytes.Contains(cell.BufferSpan)) continue; for (int i = cell.TextSpan.Start; i < cell.TextSpan.End; i++) chars[i] = ' '; } } /// /// Returns /// /// public override string ToString() => Text; } }