diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index fdd8a8001..5c74550f6 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -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 " 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 _typeVisualizers; private DebuggedProcess _process; private HostConfigurationStore _configStore; @@ -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; } @@ -1460,6 +1468,93 @@ internal static List SplitArguments(string argsText) return result; } + /// + /// Returns the index of the last top-level comma in , + /// i.e. a comma not nested inside any parentheses or square brackets. + /// Returns -1 when no such comma exists. + /// + 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; + } + + /// + /// 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. + /// + internal static string StripFormatSpecifier(string expression) + { + int commaPos = FindLastTopLevelComma(expression); + return commaPos >= 0 + ? expression.Substring(0, commaPos).TrimEnd() + : expression; + } + + /// + /// Returns the format specifier from a NatVis expression (the part after the last + /// top-level comma), normalized the same way as + /// : modifiers "nvo", "na", + /// "nr", "nd" are stripped before returning. Returns null when no specifier is present. + /// + 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", ""); + } + + /// + /// Cleans up the raw value that GDB/LLDB returns for a const char16_t* + /// expression (i.e. one evaluated with the ,sub / ,su format specifier). + /// GDB and LLDB both prefix the string with the pointer address, e.g. + /// 0x00007fff5fbff6c0 u"Hello" + /// This method strips the address and the surrounding u"…" quotes so that + /// the NatVis DisplayString shows just the string content. + /// + internal static string CleanUtf16StringValue(string value) + { + if (string.IsNullOrEmpty(value)) return value; + // Strip leading "0x " 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; + } + + /// + /// Cleans up the raw value that GDB/LLDB returns for a char* expression + /// (i.e. one evaluated with the ,sb format specifier). + /// GDB and LLDB prefix the string with the pointer address, e.g. + /// 0x00007fff5fbff6c0 "Hello" + /// This method strips the address, leaving the quoted string content. + /// + internal static string CleanAsciiStringValue(string value) + { + if (string.IsNullOrEmpty(value)) return value; + // Strip leading "0x " address prefix emitted by GDB/LLDB. + return s_addressPrefix.Replace(value, ""); + } + /// /// 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 diff --git a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs new file mode 100644 index 000000000..2b7e994b2 --- /dev/null +++ b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs @@ -0,0 +1,202 @@ +using Xunit; +using Microsoft.MIDebugEngine.Natvis; + +namespace MIDebugEngineUnitTests +{ + /// + /// Unit tests for , + /// , + /// and + /// . + /// + 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")); + } + } +}