diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8cd07616..dce1b479 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: projectPath: - test-package unityVersion: - - 2019.4.40f1 + - 2022.3.56f1 targetPlatform: - StandaloneOSX - StandaloneWindows @@ -48,5 +48,6 @@ jobs: unityVersion: ${{ matrix.unityVersion }} targetPlatform: ${{ matrix.targetPlatform }} projectPath: ${{ matrix.projectPath }}/ - allowDirtyBuild: true + allowDirtyBuild: true + testMode: editmode \ No newline at end of file diff --git a/Runtime/Model/BacktraceUnhandledException.cs b/Runtime/Model/BacktraceUnhandledException.cs index cb34c611..0309dd26 100644 --- a/Runtime/Model/BacktraceUnhandledException.cs +++ b/Runtime/Model/BacktraceUnhandledException.cs @@ -136,13 +136,13 @@ private List ConvertStackFrames(IEnumerable frames) } - //methodname index should be greater than 0 AND '(' should be before ')' - if (methodNameEndIndex < 1 && frameString[methodNameEndIndex - 1] != '(') + // we require a '(' that appears before this ')' + int openParenIndex = frameString.LastIndexOf('(', methodNameEndIndex); + if (openParenIndex == -1 || openParenIndex > methodNameEndIndex) { - result.Add(new BacktraceStackFrame() - { - FunctionName = frame - }); + // invalid shape: no matching '(' before ')' + result.Add(new BacktraceStackFrame { FunctionName = frame }); + continue; } result.Add(ConvertFrame(frameString, methodNameEndIndex)); @@ -271,63 +271,98 @@ private BacktraceStackFrame SetJITStackTraceInformation(string frameString) } /// - /// Try to convert native stack frame + /// Try to safely convert a native stack frame string into a Backtrace stack frame. + /// Handles frames with or without symbols (e.g. "0xADDR (Module)" or "0xADDR (Module) Symbol"). + /// Prevents ArgumentOutOfRangeException by validating index ranges and allowing empty symbols. /// - /// Native stack frame - /// Backtrace stack frame + /// Raw native stack frame line to parse. + /// Parsed Backtrace stack frame containing address, library, function name, and optional line number. private BacktraceStackFrame SetNativeStackTraceInformation(string frameString) { var stackFrame = new BacktraceStackFrame { - StackFrameType = Types.BacktraceStackFrameType.Native + StackFrameType = Types.BacktraceStackFrameType.Native, + FunctionName = string.Empty, + Line = 0 }; - // parse address - var addressSubstringIndex = frameString.IndexOf(' '); - if (addressSubstringIndex == -1) + + if (string.IsNullOrEmpty(frameString)) { - stackFrame.FunctionName = frameString; return stackFrame; } - stackFrame.Address = frameString.Substring(0, addressSubstringIndex); - var indexPointer = addressSubstringIndex + 1; - // parse library - if (frameString[indexPointer] == '(') + frameString = frameString.Trim(); + + // Address: starts with "0x" and ends at first space + int index = 0; + if (frameString.StartsWith("0x", StringComparison.Ordinal)) { - indexPointer = indexPointer + 1; - var libraryNameSubstringIndex = frameString.IndexOf(')', indexPointer); - stackFrame.Library = frameString.Substring(indexPointer, libraryNameSubstringIndex - indexPointer); - indexPointer = libraryNameSubstringIndex + 2; + int space = frameString.IndexOf(' '); + if (space > 2) + { + stackFrame.Address = frameString.Substring(0, space); + index = space + 1; + } + else + { + // if Unknown format keep raw text and return + stackFrame.FunctionName = frameString; + return stackFrame; + } + } + + // Library: Module + if (index < frameString.Length && frameString[index] == '(') + { + index++; + int close = frameString.IndexOf(')', index); + // if ')' missing leave Library null and continue + if (close > -1) + { + stackFrame.Library = frameString.Substring(index, close - index); + index = close + 1; + if (index < frameString.Length && frameString[index] == ' ') + { + index++; + } + } } - stackFrame.FunctionName = frameString.Substring(indexPointer); - //cleanup function name - if (stackFrame.FunctionName.StartsWith("(wrapper managed-to-native)")) + // 3) symbol + stackFrame.FunctionName = (index < frameString.Length) + ? frameString.Substring(index).Trim() + : string.Empty; + + // 4) Normalize known wrappers + if (stackFrame.FunctionName.StartsWith("(wrapper managed-to-native)", StringComparison.Ordinal)) { stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper managed-to-native)", string.Empty).Trim(); } - if (stackFrame.FunctionName.StartsWith("(wrapper runtime-invoke)")) + if (stackFrame.FunctionName.StartsWith("(wrapper runtime-invoke)", StringComparison.Ordinal)) { stackFrame.FunctionName = stackFrame.FunctionName.Replace("(wrapper runtime-invoke)", string.Empty).Trim(); } - // try to find source code information - int sourceCodeStartIndex = stackFrame.FunctionName.IndexOf('['); - int sourceCodeEndIndex = stackFrame.FunctionName.IndexOf(']'); - if (sourceCodeStartIndex != -1 && sourceCodeEndIndex != -1) + // [file:line] suffix source code information + int srcStart = stackFrame.FunctionName.IndexOf('['); + int srcEnd = stackFrame.FunctionName.IndexOf(']'); + if (srcStart != -1 && srcEnd != -1 && srcEnd > srcStart) { - sourceCodeStartIndex = sourceCodeStartIndex + 1; - var sourceCodeInformation = stackFrame.FunctionName.Substring( - sourceCodeStartIndex, - sourceCodeEndIndex - sourceCodeStartIndex); - - var sourceCodeParts = sourceCodeInformation.Split(new char[] { ':' }, 2); - if (sourceCodeParts.Length == 2) + srcStart++; + var src = stackFrame.FunctionName.Substring(srcStart, srcEnd - srcStart); + var parts = src.Split(new char[] { ':' }, 2); + if (parts.Length == 2 && int.TryParse(parts[1], out var line)) { - int.TryParse(sourceCodeParts[1], out stackFrame.Line); - stackFrame.Library = sourceCodeParts[0]; - stackFrame.FunctionName = stackFrame.FunctionName.Substring(sourceCodeEndIndex + 2); + stackFrame.Line = line; + stackFrame.Library = parts[0]; + // after ']' + int after = srcEnd + 1; + if (after < stackFrame.FunctionName.Length && stackFrame.FunctionName[after] == ' ') + { after++; } + stackFrame.FunctionName = (after < stackFrame.FunctionName.Length) + ? stackFrame.FunctionName.Substring(after) + : string.Empty; } } diff --git a/Tests/Runtime/NativeParserTests.cs b/Tests/Runtime/NativeParserTests.cs new file mode 100644 index 00000000..889a227a --- /dev/null +++ b/Tests/Runtime/NativeParserTests.cs @@ -0,0 +1,51 @@ +using Backtrace.Unity.Model; +using Backtrace.Unity.Types; +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Linq; +using System.Text; +using UnityEngine.TestTools; + +namespace Backtrace.Unity.Tests.Runtime{ + public class NativeParserTests + { + private static BacktraceStackFrame Parse(string frame) + { + var instance = new BacktraceUnhandledException("msg", ""); + var mi = typeof(BacktraceUnhandledException) + .GetMethod("SetNativeStackTraceInformation", + BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull(mi); + return (BacktraceStackFrame)mi.Invoke(instance, new object[] { frame }); + } + + [Test] + public void SymbolLessModuleLine_ParsesAddressAndLibrary_NoThrow() + { + var f = Parse("0x00007ffad7723088 (UnityPlayer)"); + Assert.AreEqual("0x00007ffad7723088", f.Address); + Assert.AreEqual("UnityPlayer", f.Library); + Assert.AreEqual(string.Empty, f.FunctionName); + Assert.AreEqual(Types.BacktraceStackFrameType.Native, f.StackFrameType); + } + + [Test] + public void WithSymbol_ParsesMethod() + { + var f = Parse("0x00007ffad7ee3c7d (UnityPlayer) UnityMain"); + Assert.AreEqual("UnityPlayer", f.Library); + Assert.AreEqual("UnityMain", f.FunctionName); + } + + [Test] + public void UnknownShape_ReturnsRawInFunctionName() + { + var f = Parse("nonsense frame with no address"); + Assert.AreEqual("nonsense frame with no address", f.FunctionName); + } + } +} diff --git a/Tests/Runtime/NativeParserTests.cs.meta b/Tests/Runtime/NativeParserTests.cs.meta new file mode 100644 index 00000000..de174c46 --- /dev/null +++ b/Tests/Runtime/NativeParserTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a99b0743e15f2473894961191792e63d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: