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
97 changes: 96 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,93 @@ 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>
/// Strips a Visual Studio NatVis format specifier (e.g. ",sub", ",d", ",Xb",
/// ",view(name)na") from the end of an expression. Format specifiers follow the
/// last top-level comma (i.e. a comma not nested inside any parentheses or
/// square brackets). GDB and LLDB do not understand these specifiers; leaving them
/// in place causes expression evaluation to fail.
/// </summary>
internal static string StripFormatSpecifier(string expression)
{
int commaPos = FindLastTopLevelComma(expression);
return commaPos >= 0
? expression.Substring(0, commaPos).TrimEnd()
: expression;
}

/// <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, leaving the quoted string content.
/// </summary>
internal static string CleanAsciiStringValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
// Strip leading "0x<hex> " address prefix emitted by GDB/LLDB.
return s_addressPrefix.Replace(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
202 changes: 202 additions & 0 deletions src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
using Xunit;
using Microsoft.MIDebugEngine.Natvis;

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

[Fact]
public void StripFormatSpecifier_NoSpecifier_Unchanged()
{
Assert.Equal("cspec == 1", Natvis.StripFormatSpecifier("cspec == 1"));
}

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

// -- simple specifiers ------------------------------------------------

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

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

[Fact]
public void StripFormatSpecifier_HexBytes_Stripped()
{
Assert.Equal("data1", Natvis.StripFormatSpecifier("data1,Xb"));
}

[Fact]
public void StripFormatSpecifier_NoVoidOmitXBytes_Stripped()
{
Assert.Equal("(data4[0])", Natvis.StripFormatSpecifier("(data4[0]),nvoXb"));
}

// -- comma inside parentheses is NOT a specifier boundary -------------

[Fact]
public void StripFormatSpecifier_CommaInsideParens_Unchanged()
{
// No top-level comma; the commas inside sizeof(...) are at depth > 0
// and must not be treated as a specifier boundary.
Assert.Equal(
"sizeof(QAtomicInt) + sizeof(int)",
Natvis.StripFormatSpecifier("sizeof(QAtomicInt) + sizeof(int)"));
}

[Fact]
public void StripFormatSpecifier_FunctionCallWithArgs_OnlySpecifierStripped()
{
// memberOffset(0),sub: comma inside parens is depth>0, top-level comma is the specifier
Assert.Equal("memberOffset(0)", Natvis.StripFormatSpecifier("memberOffset(0),sub"));
}

[Fact]
public void StripFormatSpecifier_NestedParens_OnlySpecifierStripped()
{
Assert.Equal(
"(msecs() % (24 * 60 * 60 * 1000ull))/(10 * 60 * 60 * 1000ull)",
Natvis.StripFormatSpecifier("(msecs() % (24 * 60 * 60 * 1000ull))/(10 * 60 * 60 * 1000ull),d"));
}

// -- view specifier (contains parens) ---------------------------------

[Fact]
public void StripFormatSpecifier_ViewSpecifier_Stripped()
{
// {this,view(RecZone)na}: the specifier starts at the last top-level comma
Assert.Equal("this", Natvis.StripFormatSpecifier("this,view(RecZone)na"));
}

// -- trailing whitespace trimmed --------------------------------------

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

// -- 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_AddressStripped_QuotesKept()
{
Assert.Equal("\"Hello World\"", Natvis.CleanAsciiStringValue("0x00007fff5fbff6c0 \"Hello World\""));
}

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

[Fact]
public void CleanAsciiStringValue_TruncatedNoClosingQuote_PrefixStripped()
{
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"));
}
}
}