From 4251e539f3f58869302c4b345a883f8581fdbc5d Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 16:27:31 +0100 Subject: [PATCH 01/17] feat: rename strncpy to strlcpy; added tests --- Sentry.xcodeproj/project.pbxproj | 20 +- Sources/Sentry/SentryAsyncSafeLog.c | 2 +- .../SentryCrashMonitor_CPPException.cpp | 3 +- Sources/SentryCrash/Recording/SentryCrashC.c | 2 +- .../SentryCrash/Recording/SentryCrashReport.c | 4 +- .../Recording/SentryCrashReportFixer.c | 2 +- .../Recording/Tools/SentryCrashFileUtils.c | 2 +- .../Recording/Tools/SentryCrashJSONCodec.c | 3 +- .../Filters/Tools/SentryStringUtils.h | 29 --- .../Recording/SentryCrashCTests.swift | 212 ++++++++++++++++++ .../SentryCrash/SentryCrashFileUtils_Tests.m | 82 +++++++ .../SentryCrash/SentryStringUtils.swift | 31 --- .../SentryTests/SentryTests-Bridging-Header.h | 1 - 13 files changed, 314 insertions(+), 79 deletions(-) delete mode 100644 Sources/SentryCrash/Reporting/Filters/Tools/SentryStringUtils.h create mode 100644 Tests/SentryTests/Recording/SentryCrashCTests.swift delete mode 100644 Tests/SentryTests/SentryCrash/SentryStringUtils.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index fad17aaa0dd..093a3c917b5 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -780,6 +780,7 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; + D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */; }; @@ -828,8 +829,6 @@ D84DAD5A2B1742C1003CF120 /* SentryTestUtilsDynamic.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D84DAD4D2B17428D003CF120 /* SentryTestUtilsDynamic.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D84F833D2A1CC401005828E0 /* SentrySwiftAsyncIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D84F833B2A1CC401005828E0 /* SentrySwiftAsyncIntegration.h */; }; D84F833E2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D84F833C2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m */; }; - D851527F2C9971020070F669 /* SentryStringUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = D851527E2C9971020070F669 /* SentryStringUtils.h */; }; - D85152832C997A280070F669 /* SentryStringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85152822C997A1F0070F669 /* SentryStringUtils.swift */; }; D85153002CA2B5F60070F669 /* SentrySwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8199DAA29376E9B0074249E /* SentrySwiftUI.framework */; }; D85153012CA2B5F60070F669 /* SentrySwiftUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D8199DAA29376E9B0074249E /* SentrySwiftUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D851530C2CA2B7B00070F669 /* SentryRedactModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D851530B2CA2B7A30070F669 /* SentryRedactModifierTests.swift */; }; @@ -1855,6 +1854,7 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; + D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplay.swift; sourceTree = ""; }; @@ -1907,8 +1907,6 @@ D84F833B2A1CC401005828E0 /* SentrySwiftAsyncIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySwiftAsyncIntegration.h; path = include/SentrySwiftAsyncIntegration.h; sourceTree = ""; }; D84F833C2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySwiftAsyncIntegration.m; sourceTree = ""; }; D8511F722BAC8F750015E6FD /* Sentry.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Sentry.modulemap; sourceTree = ""; }; - D851527E2C9971020070F669 /* SentryStringUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryStringUtils.h; sourceTree = ""; }; - D85152822C997A1F0070F669 /* SentryStringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStringUtils.swift; sourceTree = ""; }; D851530B2CA2B7A30070F669 /* SentryRedactModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactModifierTests.swift; sourceTree = ""; }; D85596F1280580F10041FF8B /* SentryScreenshotIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenshotIntegration.m; sourceTree = ""; }; D855AD61286ED6A4002573E1 /* SentryCrashTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCrashTests.m; sourceTree = ""; }; @@ -2503,6 +2501,7 @@ 63AA75931EB8AEDB00D153DE /* SentryTests */ = { isa = PBXGroup; children = ( + D4F2B5332D0C69CC00649E42 /* Recording */, 62872B602BA1B84400A4FA7D /* Swift */, 7B3878E92490D90400EBDEA2 /* SentryClient+TestInit.h */, D8BC83BA2AFCF08C00A662B7 /* SentryUIApplication+Private.h */, @@ -2651,7 +2650,6 @@ 63FE6FC120DA4C1000CDBAE8 /* SentryCrashVarArgs.h */, 63FE6FBF20DA4C1000CDBAE8 /* SentryDictionaryDeepSearch.h */, 63FE6FBD20DA4C1000CDBAE8 /* SentryDictionaryDeepSearch.m */, - D851527E2C9971020070F669 /* SentryStringUtils.h */, ); path = Tools; sourceTree = ""; @@ -2833,7 +2831,6 @@ 0ADC33EF28D9BE690078D980 /* TestSentryUIDeviceWrapper.swift */, 7B984A9E28E572AF001F4BEE /* CrashReport.swift */, 7BF69E062987D1FE002EBCA4 /* SentryCrashDoctorTests.swift */, - D85152822C997A1F0070F669 /* SentryStringUtils.swift */, ); path = SentryCrash; sourceTree = ""; @@ -3614,6 +3611,14 @@ name = Transaction; sourceTree = ""; }; + D4F2B5332D0C69CC00649E42 /* Recording */ = { + isa = PBXGroup; + children = ( + D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */, + ); + path = Recording; + sourceTree = ""; + }; D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( @@ -4195,7 +4200,6 @@ 7B0DC72F288698F70039995F /* NSMutableDictionary+Sentry.h in Headers */, 63FE713920DA4C1100CDBAE8 /* SentryCrashMach.h in Headers */, 63EED6BE2237923600E02400 /* SentryOptions.h in Headers */, - D851527F2C9971020070F669 /* SentryStringUtils.h in Headers */, 7BD86EC5264A63F6005439DB /* SentrySysctl.h in Headers */, 63BE85701ECEC6DE00DC44F5 /* SentryDateUtils.h in Headers */, 63FE709520DA4C1000CDBAE8 /* SentryCrashReportFilterBasic.h in Headers */, @@ -4936,6 +4940,7 @@ 7B2A70DF27D60904008B0D15 /* SentryTestThreadWrapper.swift in Sources */, 62F4DDA12C04CB9700588890 /* SentryBaggageSerializationTests.swift in Sources */, 7BE912AF272166DD00E49E62 /* SentryNoOpSpanTests.swift in Sources */, + D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */, 7B56D73524616E5600B842DA /* SentryConcurrentRateLimitsDictionaryTests.swift in Sources */, 7B7D8730248648AD00D2ECFF /* SentryStacktraceBuilderTests.swift in Sources */, 62E081AB29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift in Sources */, @@ -4953,7 +4958,6 @@ 7BA61CCF247EB59500C130A8 /* SentryCrashUUIDConversionTests.swift in Sources */, 7BBD188D2448453600427C76 /* SentryHttpDateParserTests.swift in Sources */, 7B72D23A28D074BC0014798A /* TestExtensions.swift in Sources */, - D85152832C997A280070F669 /* SentryStringUtils.swift in Sources */, 7BBD18BB24530D2600427C76 /* SentryFileManagerTests.swift in Sources */, 63FE722020DA66EC00CDBAE8 /* SentryCrashObjC_Tests.m in Sources */, 7B58816727FC5D790098B121 /* SentryDiscardReasonMapperTests.swift in Sources */, diff --git a/Sources/Sentry/SentryAsyncSafeLog.c b/Sources/Sentry/SentryAsyncSafeLog.c index c720f1019f4..5c9594eeb4d 100644 --- a/Sources/Sentry/SentryAsyncSafeLog.c +++ b/Sources/Sentry/SentryAsyncSafeLog.c @@ -140,7 +140,7 @@ sentry_asyncLogSetFileName(const char *filename, bool overwrite) fd = open(filename, openMask, 0644); unlikely_if(fd < 0) { return 1; } if (filename != g_logFilename) { - strncpy(g_logFilename, filename, sizeof(g_logFilename)); + strlcpy(g_logFilename, filename, sizeof(g_logFilename)); } } diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp index 2324995afa6..f0b9688e390 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp @@ -32,7 +32,6 @@ #include "SentryAsyncSafeLog.h" -#include "SentryStringUtils.h" #include #include #include @@ -161,7 +160,7 @@ CPPExceptionTerminate(void) try { throw; } catch (std::exception &exc) { - strncpy_safe(descriptionBuff, exc.what(), sizeof(descriptionBuff)); + strlcpy(descriptionBuff, exc.what(), sizeof(descriptionBuff)); } #define CATCH_VALUE(TYPE, PRINTFTYPE) \ catch (TYPE value) \ diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c index 3944bb44738..116357fe63b 100644 --- a/Sources/SentryCrash/Recording/SentryCrashC.c +++ b/Sources/SentryCrash/Recording/SentryCrashC.c @@ -82,7 +82,7 @@ onCrash(struct SentryCrash_MonitorContext *monitorContext) } else { char crashReportFilePath[SentryCrashFU_MAX_PATH_LENGTH]; sentrycrashcrs_getNextCrashReportPath(crashReportFilePath); - strncpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath)); + strlcpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath)); sentrycrashreport_writeStandardReport(monitorContext, crashReportFilePath); sentrySessionReplaySync_writeInfo(); } diff --git a/Sources/SentryCrash/Recording/SentryCrashReport.c b/Sources/SentryCrash/Recording/SentryCrashReport.c index 7e842ed9143..e30891e816b 100644 --- a/Sources/SentryCrash/Recording/SentryCrashReport.c +++ b/Sources/SentryCrash/Recording/SentryCrashReport.c @@ -1474,8 +1474,8 @@ sentrycrashreport_writeRecrashReport( char writeBuffer[1024]; SentryCrashBufferedWriter bufferedWriter; static char tempPath[SentryCrashFU_MAX_PATH_LENGTH]; - strncpy(tempPath, path, sizeof(tempPath) - 10); - strncpy(tempPath + strlen(tempPath) - 5, ".old", 5); + strlcpy(tempPath, path, sizeof(tempPath) - 10); + strlcpy(tempPath + strlen(tempPath) - 5, ".old", 5); SENTRY_ASYNC_SAFE_LOG_INFO("Writing recrash report to %s", path); if (rename(path, tempPath) < 0) { diff --git a/Sources/SentryCrash/Recording/SentryCrashReportFixer.c b/Sources/SentryCrash/Recording/SentryCrashReportFixer.c index 0315f7a8bb2..e4e4f1540cd 100644 --- a/Sources/SentryCrash/Recording/SentryCrashReportFixer.c +++ b/Sources/SentryCrash/Recording/SentryCrashReportFixer.c @@ -60,7 +60,7 @@ increaseDepth(FixupContext *context, const char *name) if (name == NULL) { *context->objectPath[context->currentDepth] = '\0'; } else { - strncpy(context->objectPath[context->currentDepth], name, + strlcpy(context->objectPath[context->currentDepth], name, sizeof(context->objectPath[context->currentDepth])); } context->currentDepth++; diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.c b/Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.c index 9d548fd9efe..39428ca8981 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.c @@ -161,7 +161,7 @@ deletePathContents(const char *path, bool deleteTopLevelPathAlso) for (int i = 0; i < entryCount; i++) { char *entry = entries[i]; if (entry != NULL && canDeletePath(entry)) { - strncpy(pathPtr, entry, pathRemainingLength); + strlcpy(pathPtr, entry, pathRemainingLength); deletePathContents(pathBuffer, true); } } diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c index 713b10356e8..48d9ab1f073 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c @@ -1256,8 +1256,7 @@ decodeElement(const char *const name, SentryCrashJSONDecodeContext *context) SENTRY_ASYNC_SAFE_LOG_DEBUG("Number is too long."); return SentryCrashJSON_ERROR_DATA_TOO_LONG; } - strncpy(context->stringBuffer, start, len); - context->stringBuffer[len] = '\0'; + strlcpy(context->stringBuffer, start, len); sscanf(context->stringBuffer, "%lg", &value); diff --git a/Sources/SentryCrash/Reporting/Filters/Tools/SentryStringUtils.h b/Sources/SentryCrash/Reporting/Filters/Tools/SentryStringUtils.h deleted file mode 100644 index c240ef40d52..00000000000 --- a/Sources/SentryCrash/Reporting/Filters/Tools/SentryStringUtils.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef Sentry_StringUtils_h -#define Sentry_StringUtils_h -#include - -/** - * @brief Copies a string safely ensuring null-termination. - * - * This function copies up to `n-1` characters from the `src` string to - * the `dst` buffer and ensures that the `dst` string is null-terminated. - * It behaves similarly to `strncpy`, but guarantees null-termination. - * - * @param dst The destination buffer where the string will be copied. - * @param src The source string to copy from. - * @param n The size of the destination buffer, including space for the null terminator. - * - * @return Returns the destination. - * - * @note Ensure that `n` is greater than 0. - * This can silently truncate src if it is larger than `n` - 1. - */ -static inline char * -strncpy_safe(char *dst, const char *src, size_t n) -{ - strncpy(dst, src, n - 1); - dst[n - 1] = '\0'; - return dst; -} - -#endif diff --git a/Tests/SentryTests/Recording/SentryCrashCTests.swift b/Tests/SentryTests/Recording/SentryCrashCTests.swift new file mode 100644 index 00000000000..f4bea920eff --- /dev/null +++ b/Tests/SentryTests/Recording/SentryCrashCTests.swift @@ -0,0 +1,212 @@ +// +// SentryCrashCTests.swift +// Sentry +// +// Created by Philip Niedertscheider on 13.12.24. +// Copyright © 2024 Sentry. All rights reserved. +// + +@testable import Sentry +import XCTest + +class SentryCrashCTests: XCTestCase { + + func testOnCrash_notCrashedDuringCrashHandling_shouldWriteReportToDisk() { + // -- Arrange -- + var appName = "SentryCrashCTests" + .cString(using: .utf8)! + let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("test-case-\(UUID().uuidString)") + let installDir = workDir + .appendingPathComponent(Array(repeating: "X", count: 100).joined()) + var installPath = installDir + .path + .cString(using: .utf8)! + + // Installing the sentrycrash will setup the exception handler + sentrycrash_install(&appName, &installPath) + + var monitorContext = SentryCrash_MonitorContext( + eventID: nil, + requiresAsyncSafety: false, + handlingCrash: false, + crashedDuringCrashHandling: false, + registersAreValid: false, + isStackOverflow: false, + offendingMachineContext: nil, + faultAddress: 0, + crashType: SentryCrashMonitorTypeCPPException, + exceptionName: nil, + crashReason: nil, + stackCursor: nil, + mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), + NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), + CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), + signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), + userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), + AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), + System: SentryCrash_MonitorContext.__Unnamed_struct_System(), + ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() + ) + // -- Act -- + // Calling the handle exception will trigger the onCrash handler + sentrycrashcm_handleException(&monitorContext) + + // -- Assert -- + let expectedCrashStatePath = installDir + .appendingPathComponent("Data") + .appendingPathComponent("CrashState") + .appendingPathExtension("json") + .path + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedCrashStatePath)) + } + + func testOnCrash_notCrashedDuringCrashHandling_installFilePathTooLong_shouldNotWriteToDisk() { + // -- Arrange -- + var appName = "SentryCrashCTests" + .cString(using: .utf8)! + let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("test-case-\(UUID().uuidString)") + let installDir = workDir + .appendingPathComponent(Array(repeating: "X", count: 500).joined()) + var installPath = installDir + .path + .cString(using: .utf8)! + + // Installing the sentrycrash will setup the exception handler + sentrycrash_install(&appName, &installPath) + + var monitorContext = SentryCrash_MonitorContext( + eventID: nil, + requiresAsyncSafety: false, + handlingCrash: false, + crashedDuringCrashHandling: false, + registersAreValid: false, + isStackOverflow: false, + offendingMachineContext: nil, + faultAddress: 0, + crashType: SentryCrashMonitorTypeCPPException, + exceptionName: nil, + crashReason: nil, + stackCursor: nil, + mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), + NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), + CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), + signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), + userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), + AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), + System: SentryCrash_MonitorContext.__Unnamed_struct_System(), + ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() + ) + // -- Act -- + // Calling the handle exception will trigger the onCrash handler + sentrycrashcm_handleException(&monitorContext) + + // -- Assert -- + // Check the report was written to the truncated file path + let expectedCrashStatePath = installDir + .appendingPathComponent("Data") + .appendingPathComponent("CrashState") + .appendingPathExtension("json") + XCTAssertFalse(FileManager.default.fileExists(atPath: expectedCrashStatePath.path)) + } + +// func testOnCrash_crashedDuringCrashHandling_shouldWriteReportToDisk() { +// // -- Arrange -- +// var appName = "SentryCrashCTests" +// .cString(using: .utf8)! +// let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) +// .appendingPathComponent("test-case-\(UUID().uuidString)") +// let installDir = workDir +// .appendingPathComponent(Array(repeating: "X", count: 100).joined()) +// var installPath = installDir +// .path +// .cString(using: .utf8)! +// +// // Installing the sentrycrash will setup the exception handler +// sentrycrash_install(&appName, &installPath) +// +// // -- Act -- +// // Calling the handle exception will trigger the onCrash handler +// var monitorContext = SentryCrash_MonitorContext( +// eventID: nil, +// requiresAsyncSafety: false, +// handlingCrash: false, +// crashedDuringCrashHandling: false, +// registersAreValid: false, +// isStackOverflow: false, +// offendingMachineContext: nil, +// faultAddress: 0, +// crashType: SentryCrashMonitorTypeCPPException, +// exceptionName: nil, +// crashReason: nil, +// stackCursor: nil, +// mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), +// NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), +// CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), +// signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), +// userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), +// AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), +// System: SentryCrash_MonitorContext.__Unnamed_struct_System(), +// ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() +// ) +// sentrycrashcm_handleException(&monitorContext) +// +// // Calling the handler again with 'crashedDuringCrashHandling' will rewrite the crash report +// struct SentryCrashMachineContext { +// var thisThread: thread_t // Maps directly if imported from C +// var allThreads: [thread_t] // Array to handle up to 100 threads +// var threadCount: Int +// var isCrashedContext: Bool +// var isCurrentThread: Bool +// var isStackOverflow: Bool +// var isSignalContext: Bool +// } +// var machineContext = SentryCrashMachineContext( +// thisThread: 0, +// allThreads: Array(repeating: 0, count: 100), +// threadCount: 0, +// isCrashedContext: false, +// isCurrentThread: false, +// isStackOverflow: false, +// isSignalContext: false +// ) +// withUnsafeMutablePointer(to: &machineContext) { offendingMachineContextPtr in +// SentryCrashDefaultMachineContextWrapper() +// .fillContext( +// forCurrentThread: OpaquePointer(offendingMachineContextPtr) +// ) +// monitorContext = SentryCrash_MonitorContext( +// eventID: nil, +// requiresAsyncSafety: false, +// handlingCrash: false, +// crashedDuringCrashHandling: true, +// registersAreValid: false, +// isStackOverflow: false, +// offendingMachineContext: OpaquePointer(offendingMachineContextPtr), +// faultAddress: 0, +// crashType: SentryCrashMonitorTypeCPPException, +// exceptionName: nil, +// crashReason: nil, +// stackCursor: nil, +// mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), +// NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), +// CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), +// signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), +// userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), +// AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), +// System: SentryCrash_MonitorContext.__Unnamed_struct_System(), +// ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() +// ) +// sentrycrashcm_handleException(&monitorContext) +// } +// +// // -- Assert -- +// let expectedCrashStatePath = installDir +// .appendingPathComponent("Data") +// .appendingPathComponent("CrashState") +// .appendingPathExtension("json") +// .path +// XCTAssertTrue(FileManager.default.fileExists(atPath: expectedCrashStatePath)) +// } +} diff --git a/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m index fb5f5197b4e..0ea9615e32b 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m @@ -622,4 +622,86 @@ - (void)testReadLineFromFD XCTAssertTrue(bytesRead == 0, @""); } +- (void)testDeleteContentsOfPath_canNotDeletePath_shouldNotDeleteTopLevelPath +{ + // -- Arrange -- + NSString *notDeleteablePath = @"."; + // -- Act -- + bool result = sentrycrashfu_deleteContentsOfPath([notDeleteablePath UTF8String]); + // -- Assert -- + XCTAssertFalse(result); +} + +- (void)testDeleteContentsOfPath_unknownFile_shouldNotDeleteTopLevelPath +{ + // -- Arrange -- + NSString *unknownFilePath = @"/invalid/path"; + // -- Act -- + bool result = sentrycrashfu_deleteContentsOfPath([unknownFilePath UTF8String]); + // -- Assert -- + XCTAssertFalse(result); +} + +- (void)testDeleteContentsOfPath_filePath_shouldDeleteFile +{ + // -- Arrange -- + NSString *filePath = [self.tempPath stringByAppendingPathComponent:@"test.txt"]; + NSError *error; + [@"Hello World" writeToFile:filePath + atomically:true + encoding:NSUTF8StringEncoding + error:&error]; + XCTAssertNil(error); + // Smoke-test the file got created + int fd = open([filePath UTF8String], O_RDONLY); + XCTAssertTrue(fd >= -1, "Failed to create test file"); + + // -- Act -- + bool result = sentrycrashfu_deleteContentsOfPath([filePath UTF8String]); + + // -- Assert -- + XCTAssertTrue(result); + // Validate the file got deleted + fd = open([filePath UTF8String], O_RDONLY); + XCTAssertTrue(fd == -1, "Test file was not deleted"); +} + +- (void)testDeleteContentsOfPath_dirPath_shouldDeleteAllFiles +{ + // -- Arrange -- + NSString *directoryPath = + [self.tempPath stringByAppendingPathComponent:[@"" stringByPaddingToLength:100 + withString:@"D" + startingAtIndex:0]]; + NSString *filePath = + [[directoryPath stringByAppendingPathComponent:[@"" stringByPaddingToLength:100 + withString:@"F" + startingAtIndex:0]] + stringByAppendingPathExtension:@"txt"]; + + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:directoryPath + withIntermediateDirectories:true + attributes:nil + error:&error]; + XCTAssertNil(error, "Failed to create temporary test folder"); + + [@"Hello World" writeToFile:filePath + atomically:true + encoding:NSUTF8StringEncoding + error:&error]; + XCTAssertNil(error, "Failed to create temporary test file"); + // Smoke-test the file got created + int fd = open([filePath UTF8String], O_RDONLY); + XCTAssertTrue(fd >= 0, "Failed to create test file"); + + // -- Act -- + bool result = sentrycrashfu_deleteContentsOfPath([directoryPath UTF8String]); + + // -- Assert -- + XCTAssertTrue(result); + // Validate the file got deleted + fd = open([filePath UTF8String], O_RDONLY); + XCTAssertTrue(fd == -1, "Test file was not deleted"); +} @end diff --git a/Tests/SentryTests/SentryCrash/SentryStringUtils.swift b/Tests/SentryTests/SentryCrash/SentryStringUtils.swift deleted file mode 100644 index afa894c4443..00000000000 --- a/Tests/SentryTests/SentryCrash/SentryStringUtils.swift +++ /dev/null @@ -1,31 +0,0 @@ -import XCTest - -final class SentryStringUtils: XCTestCase { - - func testStrncpy_safe_BiggerBuffer() throws { - let strn = "Hello, World!" - let dstBufferSize = strn.count + 1 - - let dst = UnsafeMutablePointer.allocate(capacity: dstBufferSize) - defer { dst.deallocate() } - - let n = try XCTUnwrap(strncpy_safe(dst, strn, dstBufferSize)) - let result = String(cString: n) - - XCTAssertEqual(result, strn) - } - - func testStrncpy_safe_smallerBuffer() throws { - let strn = "Hello, World!" - let dstBufferSize = 6 - - let dst = UnsafeMutablePointer.allocate(capacity: dstBufferSize) - defer { dst.deallocate() } - - let n = try XCTUnwrap(strncpy_safe(dst, strn, dstBufferSize)) - let result = String(cString: n) - - XCTAssertEqual(result, "Hello") - } - -} diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index a29c3704325..07e34aa39eb 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -244,4 +244,3 @@ #import "SentryCrashInstallation+Private.h" #import "SentryCrashMonitor_MachException.h" #import "SentrySessionReplaySyncC.h" -#import "SentryStringUtils.h" From 3926467e8fb0d0057a107b2ba6a1079c403a2d14 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 16:33:18 +0100 Subject: [PATCH 02/17] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5fe232fb89..ecc69fea743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - `SentrySdkInfo.packages` should be an array (#4626) +- Replace occurences of `strncpy` with `strlcpy` (#4636) ### Internal From db5da99bd133c892576b19b6beb26465882cb519 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 16 Dec 2024 14:03:33 +0100 Subject: [PATCH 03/17] add unit test for CPPException description --- .../SentryCrashMonitor_CppException_Tests.mm | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm index cb1044d80e8..ca955acf5cb 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm +++ b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm @@ -1,3 +1,4 @@ +#include "SentryCrashMonitorContext.h" #import "SentryCrashMonitor_CPPException.h" #import @@ -49,4 +50,112 @@ - (void)testCallTerminationHandler_Enabled api->setEnabled(false); } +XCTestExpectation *waitHandleExceptionHandlerExpectation; +struct SentryCrash_MonitorContext *capturedHandleExceptionContext; + +void +testHandleExceptionHandler(struct SentryCrash_MonitorContext *context) +{ + if (!context) { + XCTFail("Received null context in handler"); + return; + } + capturedHandleExceptionContext = context; + [waitHandleExceptionHandlerExpectation fulfill]; +} + +- (void)testCallHandler_shouldCaptureExceptionDescription +{ + // -- Arrange -- + waitHandleExceptionHandlerExpectation = + [self expectationWithDescription:@"Wait for C++ exception"]; + + sentrycrashcm_setEventCallback(testHandleExceptionHandler); + SentryCrashMonitorAPI *api = sentrycrashcm_cppexception_getAPI(); + + const char *errorMessage = "Example Error"; + + // -- Act -- + // Create a thread that will throw an uncaught exception + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + api->setEnabled(true); + try { + throw std::runtime_error(errorMessage); + } catch (...) { + // Rethrowing without catching will trigger std::terminate() + std::rethrow_exception(std::current_exception()); + } + }); + + // -- Assert -- + [self waitForExpectationsWithTimeout:5 + handler:^(NSError *_Nullable error) { + SentryCrash_MonitorContext *context + = capturedHandleExceptionContext; + + // Cleanup + api->setEnabled(false); + sentrycrashcm_setEventCallback(NULL); + capturedHandleExceptionContext = NULL; + + // Check for expectation failures + if (error) { + XCTFail(@"Expectation failed with error: %@", error); + } + + // Assertions + XCTAssertTrue(strcmp(context->crashReason, errorMessage)); + }]; +} + +- (void)testCallHandler_descriptionLongerThanBuffer_shouldCaptureTruncatedExceptionDescription +{ + // -- Arrange -- + waitHandleExceptionHandlerExpectation = + [self expectationWithDescription:@"Wait for C++ exception"]; + + sentrycrashcm_setEventCallback(testHandleExceptionHandler); + SentryCrashMonitorAPI *api = sentrycrashcm_cppexception_getAPI(); + + // Build a 1000 + 1 character message + NSString *errorMessage = [@"" stringByPaddingToLength:(1000 + 1) + withString:@"A" + startingAtIndex:0]; + + // -- Act -- + // Create a thread that will throw an uncaught exception + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + api->setEnabled(true); + try { + throw std::runtime_error(errorMessage.UTF8String); + } catch (...) { + // Rethrowing without catching will trigger std::terminate() + std::rethrow_exception(std::current_exception()); + } + }); + + // -- Assert -- + NSString *truncatedErrorMessage = [@"" stringByPaddingToLength:1000 + withString:@"A" + startingAtIndex:0]; + [self waitForExpectationsWithTimeout:5 + handler:^(NSError *_Nullable error) { + SentryCrash_MonitorContext *context + = capturedHandleExceptionContext; + + // Cleanup + api->setEnabled(false); + sentrycrashcm_setEventCallback(NULL); + capturedHandleExceptionContext = NULL; + + // Check for expectation failures + if (error) { + XCTFail(@"Expectation failed with error: %@", error); + } + + // Assertions + XCTAssertTrue(strcmp( + context->crashReason, truncatedErrorMessage.UTF8String)); + }]; +} @end From 49be0805b1f1b0ea2949681dffd6e37b58ef84bf Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 16 Dec 2024 15:03:01 +0100 Subject: [PATCH 04/17] add test case for deletePathContents with too long path --- .../SentryCrash/SentryCrashFileUtils_Tests.m | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m index 0ea9615e32b..34be21b2c31 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m @@ -704,4 +704,67 @@ - (void)testDeleteContentsOfPath_dirPath_shouldDeleteAllFiles fd = open([filePath UTF8String], O_RDONLY); XCTAssertTrue(fd == -1, "Test file was not deleted"); } + +/** + * The following unit test is used as a control unit test for too long paths. + * + * When the overall path length is larger than ``SentryCrashFU_MAX_PATH_LENGTH``, it the given + * buffer is not large enough to append the file entry name. This is expected to not delete folder + * for now, therefore this unit test serves as a validation for the expected behaviour. + */ +- (void)testDeleteContentsOfPath_tooLongDirPath_willNotDeleteFile +{ + // -- Arrange -- + NSFileManager *fileManager = [NSFileManager defaultManager]; + + // Create the directory + NSString *dirPath = + [[[self.tempPath stringByAppendingPathComponent:[@"" stringByPaddingToLength:200 + withString:@"A" + startingAtIndex:0]] + stringByAppendingPathComponent:[@"" stringByPaddingToLength:200 + withString:@"B" + startingAtIndex:0]] + stringByAppendingPathComponent:[@"" stringByPaddingToLength:200 + withString:@"C" + startingAtIndex:0]]; + NSError *error; + [fileManager createDirectoryAtPath:dirPath + withIntermediateDirectories:true + attributes:NULL + error:&error]; + XCTAssertNil(error); + + // Smoke-test the directory got created + bool isDirectory; + XCTAssertTrue([fileManager fileExistsAtPath:dirPath isDirectory:&isDirectory], + "Failed to create test file"); + XCTAssertTrue(isDirectory); + + // Create the test file + NSString *filePath = [dirPath stringByAppendingPathComponent:@"file.txt"]; + [@"Hello World" writeToFile:filePath + atomically:true + encoding:NSUTF8StringEncoding + error:&error]; + XCTAssertNil(error, "Failed to create temporary test file"); + + // Smoke-test the file got created + int fd = open([filePath UTF8String], O_RDONLY); + XCTAssertTrue(fd >= 0, "Failed to create test file"); + + // -- Act -- + bool result = sentrycrashfu_deleteContentsOfPath([dirPath UTF8String]); + + // -- Assert -- + XCTAssertTrue(result); + // Assert the directory was not deleted + XCTAssertTrue([fileManager fileExistsAtPath:dirPath isDirectory:&isDirectory], + "Failed to delete directory"); + XCTAssertTrue(isDirectory); + + // Assert the file was not deleted + fd = open([filePath UTF8String], O_RDONLY); + XCTAssertTrue(fd >= -1, "Failed to create test file"); +} @end From ce9668b911b6ffd6a91a4862b00c1ac6b471d7d5 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 16 Dec 2024 17:00:00 +0100 Subject: [PATCH 05/17] add unit test for oncrash write to file --- .../Recording/SentryCrashCTests.swift | 320 +++++++++--------- .../SentryThreadInspectorTests.swift | 10 +- .../SentryTests/SentryTests-Bridging-Header.h | 2 + 3 files changed, 160 insertions(+), 172 deletions(-) diff --git a/Tests/SentryTests/Recording/SentryCrashCTests.swift b/Tests/SentryTests/Recording/SentryCrashCTests.swift index f4bea920eff..9c9a8ee2af1 100644 --- a/Tests/SentryTests/Recording/SentryCrashCTests.swift +++ b/Tests/SentryTests/Recording/SentryCrashCTests.swift @@ -1,212 +1,198 @@ -// -// SentryCrashCTests.swift -// Sentry -// -// Created by Philip Niedertscheider on 13.12.24. -// Copyright © 2024 Sentry. All rights reserved. -// - @testable import Sentry import XCTest class SentryCrashCTests: XCTestCase { + func testOnCrash_notCrashedDuringCrashHandling_shouldWriteReportToDisk() throws { + // -- Arrange -- + var appName = "SentryCrashCTests" + .cString(using: .utf8)! + let installDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + var installPath = installDir + .path + .cString(using: .utf8)! + let expectedReportsDir = installDir + .appendingPathComponent("Reports") - func testOnCrash_notCrashedDuringCrashHandling_shouldWriteReportToDisk() { + // Smoke test the existence of the directory + XCTAssertFalse(FileManager.default.fileExists( + atPath: expectedReportsDir.path + )) + + // Installing the sentrycrash will setup the exception handler + sentrycrash_uninstall() + sentrycrash_install(&appName, &installPath) + + var monitorContext = SentryCrash_MonitorContext() + monitorContext.crashedDuringCrashHandling = false + + // -- Act -- + // Calling the handle exception will trigger the onCrash handler + sentrycrashcm_handleException(&monitorContext) + + // -- Assert -- + let reportUrls = try FileManager.default + .contentsOfDirectory(atPath: expectedReportsDir.path) + XCTAssertEqual(reportUrls.count, 1) + } + + func testOnCrash_notCrashedDuringCrashHandling_installFilePathTooLong_shouldNotWriteToDisk() { // -- Arrange -- var appName = "SentryCrashCTests" .cString(using: .utf8)! let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("test-case-\(UUID().uuidString)") + .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") let installDir = workDir - .appendingPathComponent(Array(repeating: "X", count: 100).joined()) + .appendingPathComponent(Array(repeating: "X", count: 500).joined()) var installPath = installDir .path .cString(using: .utf8)! + let expectedReportsDir = installDir + .appendingPathComponent("Reports") + + // Smoke test the existence of the directory + XCTAssertFalse(FileManager.default.fileExists( + atPath: expectedReportsDir.path + )) // Installing the sentrycrash will setup the exception handler + sentrycrash_uninstall() sentrycrash_install(&appName, &installPath) - var monitorContext = SentryCrash_MonitorContext( - eventID: nil, - requiresAsyncSafety: false, - handlingCrash: false, - crashedDuringCrashHandling: false, - registersAreValid: false, - isStackOverflow: false, - offendingMachineContext: nil, - faultAddress: 0, - crashType: SentryCrashMonitorTypeCPPException, - exceptionName: nil, - crashReason: nil, - stackCursor: nil, - mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), - NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), - CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), - signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), - userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), - AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), - System: SentryCrash_MonitorContext.__Unnamed_struct_System(), - ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() - ) + var monitorContext = SentryCrash_MonitorContext() + monitorContext.crashedDuringCrashHandling = false + // -- Act -- // Calling the handle exception will trigger the onCrash handler sentrycrashcm_handleException(&monitorContext) // -- Assert -- - let expectedCrashStatePath = installDir - .appendingPathComponent("Data") - .appendingPathComponent("CrashState") - .appendingPathExtension("json") + // When the path is too long, it is expected that no crash data is written to disk + XCTAssertFalse(FileManager.default.fileExists( + atPath: expectedReportsDir.path + )) + } + + func testOnCrash_crashedDuringCrashHandling_shouldWriteReportToDisk() throws { + // -- Arrange -- + var appName = "SentryCrashCTests" + .cString(using: .utf8)! + let installDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + var installPath = installDir .path - XCTAssertTrue(FileManager.default.fileExists(atPath: expectedCrashStatePath)) + .cString(using: .utf8)! + let expectedReportsDir = installDir + .appendingPathComponent("Reports") + + // Smoke test the existence of the directory + XCTAssertFalse(FileManager.default.fileExists( + atPath: expectedReportsDir.path + )) + + // Installing the sentrycrash will setup the exception handler + sentrycrash_uninstall() + sentrycrash_install(&appName, &installPath) + + // Initial Crash Context + var initialMonitorContext = SentryCrash_MonitorContext() + initialMonitorContext.crashedDuringCrashHandling = false // the first context simulates the initial crash + + // Re-created Crash + // The following crash context is a minimal version of the crash context created in the `SentryCrashMonitor_NSException` + var recrashMachineContext = SentryCrashMachineContext() + sentrycrashmc_getContextForThread( + sentrycrashthread_self(), + &recrashMachineContext, + true + ) + var cursor = SentryCrashStackCursor() + let callstack = UnsafeMutablePointer.allocate(capacity: 0) + sentrycrashsc_initWithBacktrace(&cursor, callstack, 0, 0) + + var recrashMonitorContext = SentryCrash_MonitorContext() + recrashMonitorContext.crashType = SentryCrashMonitorTypeNSException + withUnsafeMutablePointer(to: &recrashMachineContext) { ptr in + recrashMonitorContext.offendingMachineContext = ptr + } + withUnsafeMutablePointer(to: &cursor) { ptr in + recrashMonitorContext.stackCursor = UnsafeMutableRawPointer(ptr) + } + + // -- Act -- + // Calling the handle exception will trigger the onCrash handler + sentrycrashcm_handleException(&initialMonitorContext) + + // Calling the handler again with 'crashedDuringCrashHandling' will rewrite the crash report + sentrycrashcm_handleException(&recrashMonitorContext) + + // -- Assert -- + let reportUrls = try FileManager.default + .contentsOfDirectory(atPath: expectedReportsDir.path) + XCTAssertEqual( + reportUrls.count, 2 + ) } - func testOnCrash_notCrashedDuringCrashHandling_installFilePathTooLong_shouldNotWriteToDisk() { + func testOnCrash_crashedDuringCrashHandling_installFilePathTooLong_shouldNotWriteToDisk() throws { // -- Arrange -- var appName = "SentryCrashCTests" .cString(using: .utf8)! let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("test-case-\(UUID().uuidString)") + .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") let installDir = workDir .appendingPathComponent(Array(repeating: "X", count: 500).joined()) var installPath = installDir .path .cString(using: .utf8)! + let expectedReportsDir = installDir + .appendingPathComponent("Reports") + + // Smoke test the existence of the directory + XCTAssertFalse(FileManager.default.fileExists( + atPath: expectedReportsDir.path + )) // Installing the sentrycrash will setup the exception handler + sentrycrash_uninstall() sentrycrash_install(&appName, &installPath) - var monitorContext = SentryCrash_MonitorContext( - eventID: nil, - requiresAsyncSafety: false, - handlingCrash: false, - crashedDuringCrashHandling: false, - registersAreValid: false, - isStackOverflow: false, - offendingMachineContext: nil, - faultAddress: 0, - crashType: SentryCrashMonitorTypeCPPException, - exceptionName: nil, - crashReason: nil, - stackCursor: nil, - mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), - NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), - CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), - signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), - userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), - AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), - System: SentryCrash_MonitorContext.__Unnamed_struct_System(), - ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() + // Initial Crash Context + var initialMonitorContext = SentryCrash_MonitorContext() + initialMonitorContext.crashedDuringCrashHandling = false // the first context simulates the initial crash + + // Re-created Crash + // The following crash context is a minimal version of the crash context created in the `SentryCrashMonitor_NSException` + var recrashMachineContext = SentryCrashMachineContext() + sentrycrashmc_getContextForThread( + sentrycrashthread_self(), + &recrashMachineContext, + true ) + var cursor = SentryCrashStackCursor() + let callstack = UnsafeMutablePointer.allocate(capacity: 0) + sentrycrashsc_initWithBacktrace(&cursor, callstack, 0, 0) + + var recrashMonitorContext = SentryCrash_MonitorContext() + recrashMonitorContext.crashType = SentryCrashMonitorTypeNSException + withUnsafeMutablePointer(to: &recrashMachineContext) { ptr in + recrashMonitorContext.offendingMachineContext = ptr + } + withUnsafeMutablePointer(to: &cursor) { ptr in + recrashMonitorContext.stackCursor = UnsafeMutableRawPointer(ptr) + } + // -- Act -- // Calling the handle exception will trigger the onCrash handler - sentrycrashcm_handleException(&monitorContext) + sentrycrashcm_handleException(&initialMonitorContext) + + // Calling the handler again with 'crashedDuringCrashHandling' will rewrite the crash report + sentrycrashcm_handleException(&recrashMonitorContext) // -- Assert -- - // Check the report was written to the truncated file path - let expectedCrashStatePath = installDir - .appendingPathComponent("Data") - .appendingPathComponent("CrashState") - .appendingPathExtension("json") - XCTAssertFalse(FileManager.default.fileExists(atPath: expectedCrashStatePath.path)) + // When the path is too long, it is expected that no crash data is written to disk + XCTAssertFalse(FileManager.default.fileExists( + atPath: expectedReportsDir.path + )) } - -// func testOnCrash_crashedDuringCrashHandling_shouldWriteReportToDisk() { -// // -- Arrange -- -// var appName = "SentryCrashCTests" -// .cString(using: .utf8)! -// let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) -// .appendingPathComponent("test-case-\(UUID().uuidString)") -// let installDir = workDir -// .appendingPathComponent(Array(repeating: "X", count: 100).joined()) -// var installPath = installDir -// .path -// .cString(using: .utf8)! -// -// // Installing the sentrycrash will setup the exception handler -// sentrycrash_install(&appName, &installPath) -// -// // -- Act -- -// // Calling the handle exception will trigger the onCrash handler -// var monitorContext = SentryCrash_MonitorContext( -// eventID: nil, -// requiresAsyncSafety: false, -// handlingCrash: false, -// crashedDuringCrashHandling: false, -// registersAreValid: false, -// isStackOverflow: false, -// offendingMachineContext: nil, -// faultAddress: 0, -// crashType: SentryCrashMonitorTypeCPPException, -// exceptionName: nil, -// crashReason: nil, -// stackCursor: nil, -// mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), -// NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), -// CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), -// signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), -// userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), -// AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), -// System: SentryCrash_MonitorContext.__Unnamed_struct_System(), -// ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() -// ) -// sentrycrashcm_handleException(&monitorContext) -// -// // Calling the handler again with 'crashedDuringCrashHandling' will rewrite the crash report -// struct SentryCrashMachineContext { -// var thisThread: thread_t // Maps directly if imported from C -// var allThreads: [thread_t] // Array to handle up to 100 threads -// var threadCount: Int -// var isCrashedContext: Bool -// var isCurrentThread: Bool -// var isStackOverflow: Bool -// var isSignalContext: Bool -// } -// var machineContext = SentryCrashMachineContext( -// thisThread: 0, -// allThreads: Array(repeating: 0, count: 100), -// threadCount: 0, -// isCrashedContext: false, -// isCurrentThread: false, -// isStackOverflow: false, -// isSignalContext: false -// ) -// withUnsafeMutablePointer(to: &machineContext) { offendingMachineContextPtr in -// SentryCrashDefaultMachineContextWrapper() -// .fillContext( -// forCurrentThread: OpaquePointer(offendingMachineContextPtr) -// ) -// monitorContext = SentryCrash_MonitorContext( -// eventID: nil, -// requiresAsyncSafety: false, -// handlingCrash: false, -// crashedDuringCrashHandling: true, -// registersAreValid: false, -// isStackOverflow: false, -// offendingMachineContext: OpaquePointer(offendingMachineContextPtr), -// faultAddress: 0, -// crashType: SentryCrashMonitorTypeCPPException, -// exceptionName: nil, -// crashReason: nil, -// stackCursor: nil, -// mach: SentryCrash_MonitorContext.__Unnamed_struct_mach(), -// NSException: SentryCrash_MonitorContext.__Unnamed_struct_NSException(), -// CPPException: SentryCrash_MonitorContext.__Unnamed_struct_CPPException(), -// signal: SentryCrash_MonitorContext.__Unnamed_struct_signal(), -// userException: SentryCrash_MonitorContext.__Unnamed_struct_userException(), -// AppState: SentryCrash_MonitorContext.__Unnamed_struct_AppState(), -// System: SentryCrash_MonitorContext.__Unnamed_struct_System(), -// ZombieException: SentryCrash_MonitorContext.__Unnamed_struct_ZombieException() -// ) -// sentrycrashcm_handleException(&monitorContext) -// } -// -// // -- Assert -- -// let expectedCrashStatePath = installDir -// .appendingPathComponent("Data") -// .appendingPathComponent("CrashState") -// .appendingPathExtension("json") -// .path -// XCTAssertTrue(FileManager.default.fileExists(atPath: expectedCrashStatePath)) -// } } diff --git a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift index 0e419292ee0..3bc91d1059d 100644 --- a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift @@ -303,10 +303,10 @@ class SentryThreadInspectorTests: XCTestCase { private class TestSentryStacktraceBuilder: SentryStacktraceBuilder { var stackTraces = [SentryCrashThread: SentryStacktrace]() - override func buildStacktrace(forThread thread: SentryCrashThread, context: OpaquePointer) -> SentryStacktrace { + + override func buildStacktrace(forThread thread: SentryCrashThread, context: UnsafeMutablePointer) -> SentryStacktrace { return stackTraces[thread] ?? SentryStacktrace(frames: [], registers: [:]) } - } private struct ThreadInfo { @@ -316,17 +316,17 @@ private struct ThreadInfo { private class TestMachineContextWrapper: NSObject, SentryCrashMachineContextWrapper { - func fillContext(forCurrentThread context: OpaquePointer) { + func fillContext(forCurrentThread context: UnsafeMutablePointer) { // Do nothing } var threadCount: Int32 = 0 - func getThreadCount(_ context: OpaquePointer) -> Int32 { + func getThreadCount(_ context: UnsafeMutablePointer) -> Int32 { threadCount } var mockThreads: [ThreadInfo]? - func getThread(_ context: OpaquePointer, with index: Int32) -> SentryCrashThread { + func getThread(_ context: UnsafeMutablePointer, with index: Int32) -> SentryCrashThread { mockThreads?[Int(index)].threadId ?? 0 } diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 07e34aa39eb..b2351bf59f8 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -242,5 +242,7 @@ #import "SentryCrash+Test.h" #import "SentryCrashCachedData.h" #import "SentryCrashInstallation+Private.h" +#import "SentryCrashMachineContext_Apple.h" #import "SentryCrashMonitor_MachException.h" +#import "SentryCrashStackCursor_Backtrace.h" #import "SentrySessionReplaySyncC.h" From 536cc1adc2f436d109377159c24fbbf0c532924c Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 2 Jan 2025 14:41:19 +0100 Subject: [PATCH 06/17] Fix test cases --- .../Recording/Tools/SentryCrashJSONCodec.c | 2 +- .../Recording/SentryCrashCTests.swift | 84 ++++++++++++++++ .../SentryCrashMonitor_CppException_Tests.mm | 99 +++++++------------ scripts/.clang-format-version | 2 +- 4 files changed, 122 insertions(+), 65 deletions(-) diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c index 48d9ab1f073..cf41a104131 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c @@ -1256,7 +1256,7 @@ decodeElement(const char *const name, SentryCrashJSONDecodeContext *context) SENTRY_ASYNC_SAFE_LOG_DEBUG("Number is too long."); return SentryCrashJSON_ERROR_DATA_TOO_LONG; } - strlcpy(context->stringBuffer, start, len); + strncpy(context->stringBuffer, start, len); sscanf(context->stringBuffer, "%lg", &value); diff --git a/Tests/SentryTests/Recording/SentryCrashCTests.swift b/Tests/SentryTests/Recording/SentryCrashCTests.swift index 9c9a8ee2af1..3e8c84683eb 100644 --- a/Tests/SentryTests/Recording/SentryCrashCTests.swift +++ b/Tests/SentryTests/Recording/SentryCrashCTests.swift @@ -134,6 +134,71 @@ class SentryCrashCTests: XCTestCase { ) } + func testOnCrash_crashedDuringCrashHandling_shouldRewriteOldCrashAsRecrashReportToDisk() throws { + // -- Arrange -- + var appName = "SentryCrashCTests" + .cString(using: .utf8)! + let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + var installPath = workDir + .path + .cString(using: .utf8)! + let expectedReportsDir = workDir + .appendingPathComponent("Reports") + + // Smoke test the existence of the directory + XCTAssertFalse(FileManager.default.fileExists( + atPath: expectedReportsDir.path + )) + + // Installing the sentrycrash will setup the exception handler + sentrycrash_uninstall() + sentrycrash_install(&appName, &installPath) + + // Initial Crash Context + var initialMonitorContext = SentryCrash_MonitorContext() + initialMonitorContext.crashedDuringCrashHandling = false // the first context simulates the initial crash + + // Re-created Crash + // The following crash context is a minimal version of the crash context created in the `SentryCrashMonitor_NSException` + var recrashMachineContext = SentryCrashMachineContext() + sentrycrashmc_getContextForThread( + sentrycrashthread_self(), + &recrashMachineContext, + true + ) + var cursor = SentryCrashStackCursor() + let callstack = UnsafeMutablePointer.allocate(capacity: 0) + sentrycrashsc_initWithBacktrace(&cursor, callstack, 0, 0) + + var recrashMonitorContext = SentryCrash_MonitorContext() + recrashMonitorContext.crashedDuringCrashHandling = true + recrashMonitorContext.crashType = SentryCrashMonitorTypeNSException + withUnsafeMutablePointer(to: &recrashMachineContext) { ptr in + recrashMonitorContext.offendingMachineContext = ptr + } + withUnsafeMutablePointer(to: &cursor) { ptr in + recrashMonitorContext.stackCursor = UnsafeMutableRawPointer(ptr) + } + + // -- Act -- + // Calling the handle exception will trigger the onCrash handler + sentrycrashcm_handleException(&initialMonitorContext) + + // After the first handler, the report will be written to disk. + // Read it to memory now, as the next handler will edit the file. + let decodedReport = try readFirstReportFromDisk(reportsDir: expectedReportsDir) + + // Calling the handler again with 'crashedDuringCrashHandling' will rewrite the crash report + sentrycrashcm_handleException(&recrashMonitorContext) + + // -- Assert -- + let decodedRecrashReport = try readFirstReportFromDisk(reportsDir: expectedReportsDir) + + let recrashReport = decodedRecrashReport["recrash_report"] as! NSDictionary + XCTAssertEqual(recrashReport, decodedReport) + } + func testOnCrash_crashedDuringCrashHandling_installFilePathTooLong_shouldNotWriteToDisk() throws { // -- Arrange -- var appName = "SentryCrashCTests" @@ -174,6 +239,7 @@ class SentryCrashCTests: XCTestCase { sentrycrashsc_initWithBacktrace(&cursor, callstack, 0, 0) var recrashMonitorContext = SentryCrash_MonitorContext() + recrashMonitorContext.crashedDuringCrashHandling = true recrashMonitorContext.crashType = SentryCrashMonitorTypeNSException withUnsafeMutablePointer(to: &recrashMachineContext) { ptr in recrashMonitorContext.offendingMachineContext = ptr @@ -195,4 +261,22 @@ class SentryCrashCTests: XCTestCase { atPath: expectedReportsDir.path )) } + + // MARK: - Helper + + func readFirstReportFromDisk(reportsDir: URL) throws -> NSDictionary { + let reportUrls = try FileManager.default + .contentsOfDirectory(atPath: reportsDir.path) + XCTAssertEqual( reportUrls.count, 1) + XCTAssertTrue(reportUrls[0].hasPrefix("SentryCrashCTests-report-")) + XCTAssertTrue(reportUrls[0].hasSuffix(".json")) + + let reportData = try Data(contentsOf: reportsDir.appendingPathComponent(reportUrls[0])) + let decodedReport = try SentryCrashJSONCodec.decode( + reportData, + options: SentryCrashJSONDecodeOptionNone + ) as! NSDictionary + + return decodedReport + } } diff --git a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm index ca955acf5cb..cd61749ffdf 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm +++ b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm @@ -67,53 +67,36 @@ - (void)testCallTerminationHandler_Enabled - (void)testCallHandler_shouldCaptureExceptionDescription { // -- Arrange -- - waitHandleExceptionHandlerExpectation = - [self expectationWithDescription:@"Wait for C++ exception"]; - sentrycrashcm_setEventCallback(testHandleExceptionHandler); SentryCrashMonitorAPI *api = sentrycrashcm_cppexception_getAPI(); - const char *errorMessage = "Example Error"; + NSString *errorMessage = @"Example Error"; // -- Act -- - // Create a thread that will throw an uncaught exception - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - api->setEnabled(true); - try { - throw std::runtime_error(errorMessage); - } catch (...) { - // Rethrowing without catching will trigger std::terminate() - std::rethrow_exception(std::current_exception()); - } - }); + api->setEnabled(true); + try { + throw std::runtime_error(errorMessage.UTF8String); + } catch (...) { + // This exception handler sets the error context of the termination handler + // Instead of rethrowing, directly call the termination handler + std::get_terminate()(); + } // -- Assert -- - [self waitForExpectationsWithTimeout:5 - handler:^(NSError *_Nullable error) { - SentryCrash_MonitorContext *context - = capturedHandleExceptionContext; - - // Cleanup - api->setEnabled(false); - sentrycrashcm_setEventCallback(NULL); - capturedHandleExceptionContext = NULL; - - // Check for expectation failures - if (error) { - XCTFail(@"Expectation failed with error: %@", error); - } - - // Assertions - XCTAssertTrue(strcmp(context->crashReason, errorMessage)); - }]; + SentryCrash_MonitorContext *context = capturedHandleExceptionContext; + + // Cleanup + api->setEnabled(false); + sentrycrashcm_setEventCallback(NULL); + capturedHandleExceptionContext = NULL; + + NSString *crashReason = [[NSString alloc] initWithUTF8String:context->crashReason]; + XCTAssertEqualObjects(crashReason, errorMessage); } - (void)testCallHandler_descriptionLongerThanBuffer_shouldCaptureTruncatedExceptionDescription { // -- Arrange -- - waitHandleExceptionHandlerExpectation = - [self expectationWithDescription:@"Wait for C++ exception"]; - sentrycrashcm_setEventCallback(testHandleExceptionHandler); SentryCrashMonitorAPI *api = sentrycrashcm_cppexception_getAPI(); @@ -124,38 +107,28 @@ - (void)testCallHandler_descriptionLongerThanBuffer_shouldCaptureTruncatedExcept // -- Act -- // Create a thread that will throw an uncaught exception - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - api->setEnabled(true); - try { - throw std::runtime_error(errorMessage.UTF8String); - } catch (...) { - // Rethrowing without catching will trigger std::terminate() - std::rethrow_exception(std::current_exception()); - } - }); + api->setEnabled(true); + try { + throw std::runtime_error(errorMessage.UTF8String); + } catch (...) { + // This exception handler sets the error context of the termination handler + // Instead of rethrowing, directly call the termination handler + std::get_terminate()(); + } // -- Assert -- NSString *truncatedErrorMessage = [@"" stringByPaddingToLength:1000 withString:@"A" startingAtIndex:0]; - [self waitForExpectationsWithTimeout:5 - handler:^(NSError *_Nullable error) { - SentryCrash_MonitorContext *context - = capturedHandleExceptionContext; - - // Cleanup - api->setEnabled(false); - sentrycrashcm_setEventCallback(NULL); - capturedHandleExceptionContext = NULL; - - // Check for expectation failures - if (error) { - XCTFail(@"Expectation failed with error: %@", error); - } - - // Assertions - XCTAssertTrue(strcmp( - context->crashReason, truncatedErrorMessage.UTF8String)); - }]; + SentryCrash_MonitorContext *context = capturedHandleExceptionContext; + + // Cleanup + api->setEnabled(false); + sentrycrashcm_setEventCallback(NULL); + capturedHandleExceptionContext = NULL; + + // Assertions + NSString *crashReason = [[NSString alloc] initWithUTF8String:context->crashReason]; + XCTAssertEqualObjects(crashReason, truncatedErrorMessage); } @end diff --git a/scripts/.clang-format-version b/scripts/.clang-format-version index a027c639c4c..87c0f53ffeb 100644 --- a/scripts/.clang-format-version +++ b/scripts/.clang-format-version @@ -1 +1 @@ -19.1.5 +19.1.6 From e3e01277ab091269ce2e75adf3fb8ffe21a7c2df Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 2 Jan 2025 15:13:51 +0100 Subject: [PATCH 07/17] fix: testcase of cpp exception error capturing --- .../SentryCrashMonitor_CppException_Tests.mm | 91 ++++++++++++------- 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm index cd61749ffdf..25db5e5461b 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm +++ b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm @@ -12,6 +12,8 @@ @interface SentryCrashMonitor_CppException_Tests : XCTestCase @implementation SentryCrashMonitor_CppException_Tests bool terminateCalled = false; +SentryCrashMonitorAPI *api; +NSString *capturedExceptionContextCrashReason; void testTerminationHandler(void) @@ -25,34 +27,46 @@ - (void)setUp terminateCalled = false; } -- (void)testCallTerminationHandler_NotEnabled +- (void)tearDown { + [super tearDown]; + + if (api != NULL) { + api->setEnabled(false); + } + sentrycrashcm_setEventCallback(NULL); + capturedExceptionContextCrashReason = NULL; +} +- (void)testCallTerminationHandler_NotEnabled +{ + // -- Arrange -- std::set_terminate(&testTerminationHandler); + api = sentrycrashcm_cppexception_getAPI(); + + // -- Act -- sentrycrashcm_cppexception_callOriginalTerminationHandler(); + // -- Assert -- XCTAssertFalse(terminateCalled); } - (void)testCallTerminationHandler_Enabled { - + // -- Arrange -- std::set_terminate(&testTerminationHandler); - SentryCrashMonitorAPI *api = sentrycrashcm_cppexception_getAPI(); + api = sentrycrashcm_cppexception_getAPI(); api->setEnabled(true); + // -- Act -- sentrycrashcm_cppexception_callOriginalTerminationHandler(); + // -- Assert XCTAssertTrue(terminateCalled); - - api->setEnabled(false); } -XCTestExpectation *waitHandleExceptionHandlerExpectation; -struct SentryCrash_MonitorContext *capturedHandleExceptionContext; - void testHandleExceptionHandler(struct SentryCrash_MonitorContext *context) { @@ -60,19 +74,42 @@ - (void)testCallTerminationHandler_Enabled XCTFail("Received null context in handler"); return; } - capturedHandleExceptionContext = context; - [waitHandleExceptionHandlerExpectation fulfill]; + capturedExceptionContextCrashReason = [NSString stringWithUTF8String:context->crashReason]; } - (void)testCallHandler_shouldCaptureExceptionDescription { // -- Arrange -- sentrycrashcm_setEventCallback(testHandleExceptionHandler); - SentryCrashMonitorAPI *api = sentrycrashcm_cppexception_getAPI(); + api = sentrycrashcm_cppexception_getAPI(); + + // -- Act -- + api->setEnabled(true); + try { + throw std::runtime_error("Example Error"); + } catch (...) { + // This exception handler sets the error context of the termination handler + // Instead of rethrowing, directly call the termination handler + std::get_terminate()(); + } + // -- Assert -- NSString *errorMessage = @"Example Error"; + XCTAssertEqual(capturedExceptionContextCrashReason.length, errorMessage.length); + XCTAssertEqualObjects(capturedExceptionContextCrashReason, errorMessage); +} + +- (void)testCallHandler_descriptionExactLengthOfBuffer_shouldCaptureTruncatedExceptionDescription +{ + // -- Arrange -- + sentrycrashcm_setEventCallback(testHandleExceptionHandler); + api = sentrycrashcm_cppexception_getAPI(); + + // Build a 1000 + 1 character message + NSString *errorMessage = [@"" stringByPaddingToLength:1000 withString:@"A" startingAtIndex:0]; // -- Act -- + // Create a thread that will throw an uncaught exception api->setEnabled(true); try { throw std::runtime_error(errorMessage.UTF8String); @@ -83,22 +120,19 @@ - (void)testCallHandler_shouldCaptureExceptionDescription } // -- Assert -- - SentryCrash_MonitorContext *context = capturedHandleExceptionContext; - - // Cleanup - api->setEnabled(false); - sentrycrashcm_setEventCallback(NULL); - capturedHandleExceptionContext = NULL; - - NSString *crashReason = [[NSString alloc] initWithUTF8String:context->crashReason]; - XCTAssertEqualObjects(crashReason, errorMessage); + // Due to the nature of C strings, the last character of the buffer will be a null terminator + NSString *truncatedErrorMessage = [@"" stringByPaddingToLength:999 + withString:@"A" + startingAtIndex:0]; + XCTAssertEqual(capturedExceptionContextCrashReason.length, truncatedErrorMessage.length); + XCTAssertEqualObjects(capturedExceptionContextCrashReason, truncatedErrorMessage); } - (void)testCallHandler_descriptionLongerThanBuffer_shouldCaptureTruncatedExceptionDescription { // -- Arrange -- sentrycrashcm_setEventCallback(testHandleExceptionHandler); - SentryCrashMonitorAPI *api = sentrycrashcm_cppexception_getAPI(); + api = sentrycrashcm_cppexception_getAPI(); // Build a 1000 + 1 character message NSString *errorMessage = [@"" stringByPaddingToLength:(1000 + 1) @@ -117,18 +151,11 @@ - (void)testCallHandler_descriptionLongerThanBuffer_shouldCaptureTruncatedExcept } // -- Assert -- - NSString *truncatedErrorMessage = [@"" stringByPaddingToLength:1000 + // Due to the nature of C strings, the last character of the buffer will be a null terminator + NSString *truncatedErrorMessage = [@"" stringByPaddingToLength:999 withString:@"A" startingAtIndex:0]; - SentryCrash_MonitorContext *context = capturedHandleExceptionContext; - - // Cleanup - api->setEnabled(false); - sentrycrashcm_setEventCallback(NULL); - capturedHandleExceptionContext = NULL; - - // Assertions - NSString *crashReason = [[NSString alloc] initWithUTF8String:context->crashReason]; - XCTAssertEqualObjects(crashReason, truncatedErrorMessage); + XCTAssertEqual(capturedExceptionContextCrashReason.length, truncatedErrorMessage.length); + XCTAssertEqualObjects(capturedExceptionContextCrashReason, truncatedErrorMessage); } @end From ef8f61940dcfe755d00b344d4b616678cc4801e1 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 2 Jan 2025 15:15:27 +0100 Subject: [PATCH 08/17] fix: fix replacement in SentryCrashJSONCodec --- Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c index cf41a104131..48d9ab1f073 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c @@ -1256,7 +1256,7 @@ decodeElement(const char *const name, SentryCrashJSONDecodeContext *context) SENTRY_ASYNC_SAFE_LOG_DEBUG("Number is too long."); return SentryCrashJSON_ERROR_DATA_TOO_LONG; } - strncpy(context->stringBuffer, start, len); + strlcpy(context->stringBuffer, start, len); sscanf(context->stringBuffer, "%lg", &value); From b8efac12b73ff9f39ad38e8e835023d80ec5b67b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 2 Jan 2025 15:16:48 +0100 Subject: [PATCH 09/17] fix: changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e2df7d171..0d06e54f1eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Fixes - Remove empty session replay tags (#4667) - +- Replace occurences of `strncpy` with `strlcpy` (#4636) ## 8.43.0-beta.1 @@ -27,7 +27,6 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) -- Replace occurences of `strncpy` with `strlcpy` (#4636) ### Internal From 6c67afd98965ed8eec9c26f6be4eb6bd0ff315ab Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 2 Jan 2025 15:46:33 +0100 Subject: [PATCH 10/17] fix: add comments why strncpy needs to be used for jsoncodec decoding --- .../Recording/Tools/SentryCrashJSONCodec.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c index 48d9ab1f073..58a3d5baea7 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c @@ -1256,8 +1256,18 @@ decodeElement(const char *const name, SentryCrashJSONDecodeContext *context) SENTRY_ASYNC_SAFE_LOG_DEBUG("Number is too long."); return SentryCrashJSON_ERROR_DATA_TOO_LONG; } - strlcpy(context->stringBuffer, start, len); - + // Must use strncpy instead of strlcpy, because of the following reason: + // + // Also note that strlcpy() and strlcat() only operate on true ''C'' strings. + // This means that for strlcpy() src must be NUL-terminated and for strlcat() + // both src and dst must be NUL-terminated. + // + // Source: https://linux.die.net/man/3/strlcpy + strncpy(context->stringBuffer, start, len); + context->stringBuffer[len] = '\0'; + + // Parses a floating point number from stringBuffer into value using %lg format + // %lg uses shortest decimal representation and removes trailing zeros sscanf(context->stringBuffer, "%lg", &value); value *= sign; From bce122ce683d5562b82349e410f6b7eda97e9fbb Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 11:30:08 +0100 Subject: [PATCH 11/17] fix merge conflict in changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5262e1fe8ce..6e9d9cc8398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Replace occurences of `strncpy` with `strlcpy` (#4636) + ### Internal - Update to Xcode 16.2 in workflows (#4673) @@ -22,6 +23,19 @@ ### Fixes - Remove empty session replay tags (#4667) +- - `SentrySdkInfo.packages` should be an array (#4626) +- Use the same SdkInfo for envelope header and event (#4629) + +### Improvements + +- Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) +- Mask screenshots for errors (#4623) +- Slightly speed up serializing scope (#4661) + +### Internal + +- Remove loading `integrations` names from `event.extra` (#4627) +- Add Hybrid SDKs API to add extra SDK packages (#4637) ## 8.43.0-beta.1 From 923aa32a99a751ac032c3f760b04573f5937dd79 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 11:46:42 +0100 Subject: [PATCH 12/17] rename test handler to mock; add truncation check in test --- .../SentryCrashMonitor_CppException_Tests.mm | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm index 25db5e5461b..7d1bd2113f2 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm +++ b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm @@ -16,7 +16,7 @@ @implementation SentryCrashMonitor_CppException_Tests NSString *capturedExceptionContextCrashReason; void -testTerminationHandler(void) +mockTerminationHandler(void) { terminateCalled = true; } @@ -25,6 +25,8 @@ - (void)setUp { [super setUp]; terminateCalled = false; + + api = sentrycrashcm_cppexception_getAPI(); } - (void)tearDown @@ -41,9 +43,7 @@ - (void)tearDown - (void)testCallTerminationHandler_NotEnabled { // -- Arrange -- - std::set_terminate(&testTerminationHandler); - - api = sentrycrashcm_cppexception_getAPI(); + std::set_terminate(&mockTerminationHandler); // -- Act -- sentrycrashcm_cppexception_callOriginalTerminationHandler(); @@ -55,9 +55,7 @@ - (void)testCallTerminationHandler_NotEnabled - (void)testCallTerminationHandler_Enabled { // -- Arrange -- - std::set_terminate(&testTerminationHandler); - - api = sentrycrashcm_cppexception_getAPI(); + std::set_terminate(&mockTerminationHandler); api->setEnabled(true); // -- Act -- @@ -68,7 +66,7 @@ - (void)testCallTerminationHandler_Enabled } void -testHandleExceptionHandler(struct SentryCrash_MonitorContext *context) +mockHandleExceptionHandler(struct SentryCrash_MonitorContext *context) { if (!context) { XCTFail("Received null context in handler"); @@ -80,11 +78,10 @@ - (void)testCallTerminationHandler_Enabled - (void)testCallHandler_shouldCaptureExceptionDescription { // -- Arrange -- - sentrycrashcm_setEventCallback(testHandleExceptionHandler); - api = sentrycrashcm_cppexception_getAPI(); + sentrycrashcm_setEventCallback(mockHandleExceptionHandler); + api->setEnabled(true); // -- Act -- - api->setEnabled(true); try { throw std::runtime_error("Example Error"); } catch (...) { @@ -102,15 +99,13 @@ - (void)testCallHandler_shouldCaptureExceptionDescription - (void)testCallHandler_descriptionExactLengthOfBuffer_shouldCaptureTruncatedExceptionDescription { // -- Arrange -- - sentrycrashcm_setEventCallback(testHandleExceptionHandler); - api = sentrycrashcm_cppexception_getAPI(); + sentrycrashcm_setEventCallback(mockHandleExceptionHandler); + api->setEnabled(true); // Build a 1000 + 1 character message NSString *errorMessage = [@"" stringByPaddingToLength:1000 withString:@"A" startingAtIndex:0]; // -- Act -- - // Create a thread that will throw an uncaught exception - api->setEnabled(true); try { throw std::runtime_error(errorMessage.UTF8String); } catch (...) { @@ -131,17 +126,15 @@ - (void)testCallHandler_descriptionExactLengthOfBuffer_shouldCaptureTruncatedExc - (void)testCallHandler_descriptionLongerThanBuffer_shouldCaptureTruncatedExceptionDescription { // -- Arrange -- - sentrycrashcm_setEventCallback(testHandleExceptionHandler); - api = sentrycrashcm_cppexception_getAPI(); + sentrycrashcm_setEventCallback(mockHandleExceptionHandler); + api->setEnabled(true); - // Build a 1000 + 1 character message - NSString *errorMessage = [@"" stringByPaddingToLength:(1000 + 1) - withString:@"A" - startingAtIndex:0]; + // Build a 1000 character message, with a single character overflow. + // The overflow character is different, so that we can verify truncation at the end + NSString *errorMessage = [[@"" stringByPaddingToLength:1000 withString:@"A" + startingAtIndex:0] stringByAppendingString:@"B"]; // -- Act -- - // Create a thread that will throw an uncaught exception - api->setEnabled(true); try { throw std::runtime_error(errorMessage.UTF8String); } catch (...) { From 889c4518ac42ef559e48ddfa0167e050ca58bb32 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 11:47:57 +0100 Subject: [PATCH 13/17] Remove copypasta Co-authored-by: Andrew McKnight --- .../SentryCrash/SentryCrashMonitor_CppException_Tests.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm index 7d1bd2113f2..f5903b39d6d 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm +++ b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_CppException_Tests.mm @@ -102,7 +102,7 @@ - (void)testCallHandler_descriptionExactLengthOfBuffer_shouldCaptureTruncatedExc sentrycrashcm_setEventCallback(mockHandleExceptionHandler); api->setEnabled(true); - // Build a 1000 + 1 character message + // Build a 1000 character message NSString *errorMessage = [@"" stringByPaddingToLength:1000 withString:@"A" startingAtIndex:0]; // -- Act -- From ecf7e80cc284a683ea253361aa6124bd88d2e4a0 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 11:51:30 +0100 Subject: [PATCH 14/17] remove ununecessary docs --- Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c index 58a3d5baea7..a8fd78c89cc 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c @@ -1258,15 +1258,14 @@ decodeElement(const char *const name, SentryCrashJSONDecodeContext *context) } // Must use strncpy instead of strlcpy, because of the following reason: // - // Also note that strlcpy() and strlcat() only operate on true ''C'' strings. - // This means that for strlcpy() src must be NUL-terminated and for strlcat() - // both src and dst must be NUL-terminated. + // Also note that strlcpy() and strlcat() only operate on true 'C' strings. + // This means that for strlcpy() src must be NUL-terminated [..] // // Source: https://linux.die.net/man/3/strlcpy strncpy(context->stringBuffer, start, len); context->stringBuffer[len] = '\0'; - // Parses a floating point number from stringBuffer into value using %lg format + // Parses a floating point number from the string buffer into value using %lg format // %lg uses shortest decimal representation and removes trailing zeros sscanf(context->stringBuffer, "%lg", &value); From ee88fa43abcd116fdf485407a943b85a4d42c7ab Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 11:55:57 +0100 Subject: [PATCH 15/17] compact vertical spacing --- .../Recording/SentryCrashCTests.swift | 76 ++++++------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/Tests/SentryTests/Recording/SentryCrashCTests.swift b/Tests/SentryTests/Recording/SentryCrashCTests.swift index 3e8c84683eb..5322336d0ad 100644 --- a/Tests/SentryTests/Recording/SentryCrashCTests.swift +++ b/Tests/SentryTests/Recording/SentryCrashCTests.swift @@ -4,15 +4,10 @@ import XCTest class SentryCrashCTests: XCTestCase { func testOnCrash_notCrashedDuringCrashHandling_shouldWriteReportToDisk() throws { // -- Arrange -- - var appName = "SentryCrashCTests" - .cString(using: .utf8)! - let installDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") - var installPath = installDir - .path - .cString(using: .utf8)! - let expectedReportsDir = installDir - .appendingPathComponent("Reports") + var appName = "SentryCrashCTests".cString(using: .utf8)! + let installDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + var installPath = installDir.path.cString(using: .utf8)! + let expectedReportsDir = installDir.appendingPathComponent("Reports") // Smoke test the existence of the directory XCTAssertFalse(FileManager.default.fileExists( @@ -38,17 +33,11 @@ class SentryCrashCTests: XCTestCase { func testOnCrash_notCrashedDuringCrashHandling_installFilePathTooLong_shouldNotWriteToDisk() { // -- Arrange -- - var appName = "SentryCrashCTests" - .cString(using: .utf8)! - let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") - let installDir = workDir - .appendingPathComponent(Array(repeating: "X", count: 500).joined()) - var installPath = installDir - .path - .cString(using: .utf8)! - let expectedReportsDir = installDir - .appendingPathComponent("Reports") + var appName = "SentryCrashCTests".cString(using: .utf8)! + let workDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + let installDir = workDir.appendingPathComponent(Array(repeating: "X", count: 500).joined()) + var installPath = installDir.path.cString(using: .utf8)! + let expectedReportsDir = installDir.appendingPathComponent("Reports") // Smoke test the existence of the directory XCTAssertFalse(FileManager.default.fileExists( @@ -75,15 +64,10 @@ class SentryCrashCTests: XCTestCase { func testOnCrash_crashedDuringCrashHandling_shouldWriteReportToDisk() throws { // -- Arrange -- - var appName = "SentryCrashCTests" - .cString(using: .utf8)! - let installDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") - var installPath = installDir - .path - .cString(using: .utf8)! - let expectedReportsDir = installDir - .appendingPathComponent("Reports") + var appName = "SentryCrashCTests".cString(using: .utf8)! + let installDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + var installPath = installDir.path.cString(using: .utf8)! + let expectedReportsDir = installDir.appendingPathComponent("Reports") // Smoke test the existence of the directory XCTAssertFalse(FileManager.default.fileExists( @@ -136,15 +120,10 @@ class SentryCrashCTests: XCTestCase { func testOnCrash_crashedDuringCrashHandling_shouldRewriteOldCrashAsRecrashReportToDisk() throws { // -- Arrange -- - var appName = "SentryCrashCTests" - .cString(using: .utf8)! - let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") - var installPath = workDir - .path - .cString(using: .utf8)! - let expectedReportsDir = workDir - .appendingPathComponent("Reports") + var appName = "SentryCrashCTests".cString(using: .utf8)! + let workDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + var installPath = workDir.path.cString(using: .utf8)! + let expectedReportsDir = workDir.appendingPathComponent("Reports") // Smoke test the existence of the directory XCTAssertFalse(FileManager.default.fileExists( @@ -201,17 +180,11 @@ class SentryCrashCTests: XCTestCase { func testOnCrash_crashedDuringCrashHandling_installFilePathTooLong_shouldNotWriteToDisk() throws { // -- Arrange -- - var appName = "SentryCrashCTests" - .cString(using: .utf8)! - let workDir = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") - let installDir = workDir - .appendingPathComponent(Array(repeating: "X", count: 500).joined()) - var installPath = installDir - .path - .cString(using: .utf8)! - let expectedReportsDir = installDir - .appendingPathComponent("Reports") + var appName = "SentryCrashCTests".cString(using: .utf8)! + let workDir = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("SentryCrashCTests-\(UUID().uuidString)") + let installDir = workDir.appendingPathComponent(Array(repeating: "X", count: 500).joined()) + var installPath = installDir.path.cString(using: .utf8)! + let expectedReportsDir = installDir.appendingPathComponent("Reports") // Smoke test the existence of the directory XCTAssertFalse(FileManager.default.fileExists( @@ -265,9 +238,8 @@ class SentryCrashCTests: XCTestCase { // MARK: - Helper func readFirstReportFromDisk(reportsDir: URL) throws -> NSDictionary { - let reportUrls = try FileManager.default - .contentsOfDirectory(atPath: reportsDir.path) - XCTAssertEqual( reportUrls.count, 1) + let reportUrls = try FileManager.default.contentsOfDirectory(atPath: reportsDir.path) + XCTAssertEqual(reportUrls.count, 1) XCTAssertTrue(reportUrls[0].hasPrefix("SentryCrashCTests-report-")) XCTAssertTrue(reportUrls[0].hasSuffix(".json")) From 80f5dc98e5bca2bdc48a1cc604741171eec18db2 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 11:56:24 +0100 Subject: [PATCH 16/17] Update Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m Co-authored-by: Andrew McKnight --- Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m index 34be21b2c31..2746ad72bea 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashFileUtils_Tests.m @@ -708,7 +708,7 @@ - (void)testDeleteContentsOfPath_dirPath_shouldDeleteAllFiles /** * The following unit test is used as a control unit test for too long paths. * - * When the overall path length is larger than ``SentryCrashFU_MAX_PATH_LENGTH``, it the given + * When the overall path length is larger than ``SentryCrashFU_MAX_PATH_LENGTH``, the given * buffer is not large enough to append the file entry name. This is expected to not delete folder * for now, therefore this unit test serves as a validation for the expected behaviour. */ From 820d18b323647bfdf07ae57dad263910113693c3 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:51:31 +0100 Subject: [PATCH 17/17] add workflow dispatch for testflight upload --- .github/workflows/testflight.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index fbc46498492..b17283b8978 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -14,6 +14,7 @@ on: pull_request: paths: - '.github/workflows/testflight.yml' + workflow_dispatch: jobs: upload_to_testflight: