Skip to content
Merged
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
92 changes: 91 additions & 1 deletion src/MIDebugEngine/Natvis.Impl/Natvis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ public VisualizerInfo(VisualizerType viz, TypeName name)
private static Regex s_expression = new Regex(@"^\{[^\}]*\}");
private static readonly Regex s_moduleQualifiedPrefix = new Regex(@"\w+(?:\.\w+)*\.(?:dll|exe)!", RegexOptions.IgnoreCase);
private static readonly Regex s_intrinsicCallPattern = new Regex(@"\b(\w+)\s*\(");
// Matches the leading "0x<hex> " address that GDB/LLDB prepends when displaying a string pointer value.
private static readonly Regex s_addressPrefix = new Regex(@"^0x[0-9a-fA-F]+\s+");
private List<FileInfo> _typeVisualizers;
private DebuggedProcess _process;
private HostConfigurationStore _configStore;
Expand Down Expand Up @@ -1280,7 +1282,13 @@ private string FormatValue(string format, IVariableInformation variable, IDictio
Match m = s_expression.Match(format.Substring(i));
if (m.Success)
{
string exprValue = GetExpressionValue(format.Substring(i + 1, m.Length - 2), variable, scopedNames, intrinsics);
string rawExpr = format.Substring(i + 1, m.Length - 2);
string spec = ExtractFormatSpecifier(rawExpr);
string exprValue = GetExpressionValue(rawExpr, variable, scopedNames, intrinsics);
if (spec == "sub" || spec == "su")
exprValue = CleanUtf16StringValue(exprValue);
else if (spec == "sb")
exprValue = CleanAsciiStringValue(exprValue);
value.Append(exprValue);
i += m.Length - 1;
}
Expand Down Expand Up @@ -1460,6 +1468,88 @@ internal static List<string> SplitArguments(string argsText)
return result;
}

/// <summary>
/// Returns the index of the last top-level comma in <paramref name="expression"/>,
/// i.e. a comma not nested inside any parentheses or square brackets.
/// Returns -1 when no such comma exists.
/// </summary>
private static int FindLastTopLevelComma(string expression)
{
int depth = 0;
int lastTopLevelComma = -1;
for (int i = 0; i < expression.Length; i++)
{
char c = expression[i];
if (c == '(' || c == '[') depth++;
else if (c == ')' || c == ']') depth--;
else if (c == ',' && depth == 0)
lastTopLevelComma = i;
}
return lastTopLevelComma;
}

/// <summary>
/// Returns the format specifier from a NatVis expression (the part after the last
/// top-level comma), normalized the same way as
/// <see cref="VariableInformation.ProcessFormatSpecifiers"/>: modifiers "nvo", "na",
/// "nr", "nd" are stripped before returning. Returns null when no specifier is present.
/// </summary>
internal static string ExtractFormatSpecifier(string expression)
{
int commaPos = FindLastTopLevelComma(expression);
if (commaPos < 0) return null;
return expression.Substring(commaPos + 1).Trim()
.Replace("nvo", "").Replace("na", "").Replace("nr", "").Replace("nd", "");
}

/// <summary>
/// Cleans up the raw value that GDB/LLDB returns for a <c>const char16_t*</c>
/// expression (i.e. one evaluated with the <c>,sub</c> / <c>,su</c> format specifier).
/// GDB and LLDB both prefix the string with the pointer address, e.g.
/// <c>0x00007fff5fbff6c0 u"Hello"</c>
/// This method strips the address and the surrounding <c>u"…"</c> quotes so that
/// the NatVis DisplayString shows just the string content.
/// </summary>
internal static string CleanUtf16StringValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
// Strip leading "0x<hex> " address prefix emitted by GDB/LLDB.
value = s_addressPrefix.Replace(value, "");
// Strip surrounding u"..." or U"..." quotes.
if (value.Length >= 3 &&
(value.StartsWith("u\"", StringComparison.Ordinal) || value.StartsWith("U\"", StringComparison.Ordinal)))
{
value = value.EndsWith("\"", StringComparison.Ordinal)
? value.Substring(2, value.Length - 3)
: value.Substring(2);
}
return value;
}

/// <summary>
/// Cleans up the raw value that GDB/LLDB returns for a <c>char*</c> expression
/// (i.e. one evaluated with the <c>,sb</c> format specifier).
/// GDB and LLDB prefix the string with the pointer address, e.g.
/// <c>0x00007fff5fbff6c0 "Hello"</c>
/// This method strips the address and the surrounding <c>"…"</c> quotes so that
/// the NatVis DisplayString shows just the string content (matching VS behaviour,
/// where <c>{ptr,sb}</c> evaluates to bare text without quotes).
/// </summary>
internal static string CleanAsciiStringValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
// Strip leading "0x<hex> " address prefix emitted by GDB/LLDB.
value = s_addressPrefix.Replace(value, "");
// Strip surrounding "..." quotes.
if (value.Length >= 2 && value.StartsWith("\"", StringComparison.Ordinal))
{
value = value.EndsWith("\"", StringComparison.Ordinal)
? value.Substring(1, value.Length - 2)
: value.Substring(1);
}
return value;
}

/// <summary>
/// Substitute named parameters in an intrinsic expression with the supplied argument
/// values. Each parameter name is replaced as a whole word so that e.g. "val" inside
Expand Down
117 changes: 117 additions & 0 deletions src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using Xunit;
using Microsoft.MIDebugEngine.Natvis;

namespace MIDebugEngineUnitTests
{
/// <summary>
/// Unit tests for <see cref="Natvis.ExtractFormatSpecifier"/>,
/// <see cref="Natvis.CleanUtf16StringValue"/> and
/// <see cref="Natvis.CleanAsciiStringValue"/>.
/// </summary>
public class NatvisFormatSpecifierTest
{
// -- ExtractFormatSpecifier -------------------------------------------

[Fact]
public void ExtractFormatSpecifier_Sub_Extracted()
{
Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub"));
}

[Fact]
public void ExtractFormatSpecifier_Decimal_Extracted()
{
Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d"));
}

[Fact]
public void ExtractFormatSpecifier_NoSpecifier_ReturnsNull()
{
Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1"));
}

[Fact]
public void ExtractFormatSpecifier_NvoModifierStripped()
{
// "nvoXb": strip "nvo" modifier, result is "Xb"
Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb"));
}

[Fact]
public void ExtractFormatSpecifier_NaModifierStripped()
{
// "view(RecZone)na": strip "na", result is "view(RecZone)"
Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na"));
}

// -- CleanUtf16StringValue --------------------------------------------

[Fact]
public void CleanUtf16StringValue_AddressAndQuotes_Stripped()
{
Assert.Equal("Hello World", Natvis.CleanUtf16StringValue("0x00007fff5fbff6c0 u\"Hello World\""));
}

[Fact]
public void CleanUtf16StringValue_NoAddress_QuotesStripped()
{
Assert.Equal("Hello", Natvis.CleanUtf16StringValue("u\"Hello\""));
}

[Fact]
public void CleanUtf16StringValue_UpperCaseU_QuotesStripped()
{
Assert.Equal("Hello", Natvis.CleanUtf16StringValue("U\"Hello\""));
}

[Fact]
public void CleanUtf16StringValue_TruncatedNoClosingQuote_PrefixStripped()
{
Assert.Equal("Hello...", Natvis.CleanUtf16StringValue("0x00007fff u\"Hello..."));
}

[Fact]
public void CleanUtf16StringValue_Empty_ReturnsEmpty()
{
Assert.Equal("", Natvis.CleanUtf16StringValue(""));
}

[Fact]
public void CleanUtf16StringValue_NoPrefix_Unchanged()
{
Assert.Equal("42", Natvis.CleanUtf16StringValue("42"));
}

// -- CleanAsciiStringValue --------------------------------------------

[Fact]
public void CleanAsciiStringValue_AddressAndQuotes_Stripped()
{
Assert.Equal("Hello World", Natvis.CleanAsciiStringValue("0x00007fff5fbff6c0 \"Hello World\""));
}

[Fact]
public void CleanAsciiStringValue_NoAddress_QuotesStripped()
{
Assert.Equal("Hello", Natvis.CleanAsciiStringValue("\"Hello\""));
}

[Fact]
public void CleanAsciiStringValue_TruncatedNoClosingQuote_PrefixAndOpenQuoteStripped()
{
Assert.Equal("Hello...", Natvis.CleanAsciiStringValue("0x00007fff \"Hello..."));
}

[Fact]
public void CleanAsciiStringValue_Empty_ReturnsEmpty()
{
Assert.Equal("", Natvis.CleanAsciiStringValue(""));
}

[Fact]
public void CleanAsciiStringValue_NoPrefix_Unchanged()
{
Assert.Equal("42", Natvis.CleanAsciiStringValue("42"));
}
}
}
Loading