diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c index 6d9bff4cf95..df5c70b7b63 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c @@ -317,34 +317,54 @@ sentrycrashjson_addFloatingPointElement( if (isnan(value)) { return sentrycrashjson_addNullElement(context, name); } + if (isinf(value)) { + int result = sentrycrashjson_beginElement(context, name); + unlikely_if(result != SentryCrashJSON_OK) { return result; } + return value > 0 ? addJSONData(context, "1e999", 5) : addJSONData(context, "-1e999", 6); + } + char buff[50]; + int written = snprintf(buff, sizeof(buff), "%lg", value); + if (written < 0) { + return SentryCrashJSON_ERROR_INVALID_CHARACTER; + } else if (written >= (int)sizeof(buff)) { + return SentryCrashJSON_ERROR_DATA_TOO_LONG; + } int result = sentrycrashjson_beginElement(context, name); unlikely_if(result != SentryCrashJSON_OK) { return result; } - char buff[50]; - snprintf(buff, sizeof(buff), "%lg", value); - return addJSONData(context, buff, (int)strlen(buff)); + return addJSONData(context, buff, written); } int sentrycrashjson_addIntegerElement( SentryCrashJSONEncodeContext *const context, const char *const name, int64_t value) { + char buff[30]; + int written = snprintf(buff, sizeof(buff), "%" PRId64, value); + if (written < 0) { + return SentryCrashJSON_ERROR_INVALID_CHARACTER; + } else if (written >= (int)sizeof(buff)) { + return SentryCrashJSON_ERROR_DATA_TOO_LONG; + } int result = sentrycrashjson_beginElement(context, name); unlikely_if(result != SentryCrashJSON_OK) { return result; } - char buff[30]; - snprintf(buff, sizeof(buff), "%" PRId64, value); - return addJSONData(context, buff, (int)strlen(buff)); + return addJSONData(context, buff, written); } int sentrycrashjson_addUIntegerElement( SentryCrashJSONEncodeContext *const context, const char *const name, uint64_t value) { + char buff[30]; + int written = snprintf(buff, sizeof(buff), "%" PRIu64, value); + if (written < 0) { + return SentryCrashJSON_ERROR_INVALID_CHARACTER; + } else if (written >= (int)sizeof(buff)) { + return SentryCrashJSON_ERROR_DATA_TOO_LONG; + } int result = sentrycrashjson_beginElement(context, name); unlikely_if(result != SentryCrashJSON_OK) { return result; } - char buff[30]; - snprintf(buff, sizeof(buff), "%" PRIu64, value); - return addJSONData(context, buff, (int)strlen(buff)); + return addJSONData(context, buff, written); } int diff --git a/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift b/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift index 320743ef75e..195ec4f9efd 100644 --- a/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift +++ b/Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift @@ -132,12 +132,14 @@ final class SentrySystemEventBreadcrumbs: NSObject { let currentLevel = currentDevice.batteryLevel var batteryData: [String: Any] = [:] - // W3C spec says level must be null if it is unknown - if currentState != .unknown && currentLevel != -1.0 { + // W3C spec says level must be null if it is unknown. + // Also guard against non-finite or out-of-range values so they never + // reach the crash-scope JSON encoder as invalid floats. + if currentState != .unknown && currentLevel.isFinite && (0...1).contains(currentLevel) { let w3cLevel = currentLevel * 100 batteryData["level"] = NSNumber(value: w3cLevel) } else { - SentrySDKLog.debug("batteryLevel is unknown.") + SentrySDKLog.debug("batteryLevel is unknown or has unexpected value: \(currentLevel)") } batteryData["plugged"] = isPlugged diff --git a/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift b/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift index 0c0aa09a533..4405c99ccfa 100644 --- a/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift +++ b/Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift @@ -144,6 +144,59 @@ class SentrySystemEventBreadcrumbsTest: XCTestCase { assertBatteryBreadcrumb(charging: false, level: 100) } + func testBatteryInfinityLevelOmitsLevel() { + let currentDevice = MyUIDevice(batteryLevel: .infinity, batteryState: .charging) + + sut = fixture.getSut(currentDevice: currentDevice) + + postBatteryLevelNotification(uiDevice: currentDevice) + + XCTAssertEqual(1, fixture.delegate.addCrumbInvocations.count) + if let data = fixture.delegate.addCrumbInvocations.first?.data { + XCTAssertNil(data["level"], "Infinity battery level should be omitted") + XCTAssertEqual("BATTERY_STATE_CHANGE", data["action"] as? String) + } + } + + func testBatteryNegativeInfinityLevelOmitsLevel() { + let currentDevice = MyUIDevice(batteryLevel: -.infinity, batteryState: .charging) + + sut = fixture.getSut(currentDevice: currentDevice) + + postBatteryLevelNotification(uiDevice: currentDevice) + + XCTAssertEqual(1, fixture.delegate.addCrumbInvocations.count) + if let data = fixture.delegate.addCrumbInvocations.first?.data { + XCTAssertNil(data["level"], "Negative infinity battery level should be omitted") + } + } + + func testBatteryNaNLevelOmitsLevel() { + let currentDevice = MyUIDevice(batteryLevel: .nan, batteryState: .charging) + + sut = fixture.getSut(currentDevice: currentDevice) + + postBatteryLevelNotification(uiDevice: currentDevice) + + XCTAssertEqual(1, fixture.delegate.addCrumbInvocations.count) + if let data = fixture.delegate.addCrumbInvocations.first?.data { + XCTAssertNil(data["level"], "NaN battery level should be omitted") + } + } + + func testBatteryOutOfRangeLevelOmitsLevel() { + let currentDevice = MyUIDevice(batteryLevel: 1.5, batteryState: .charging) + + sut = fixture.getSut(currentDevice: currentDevice) + + postBatteryLevelNotification(uiDevice: currentDevice) + + XCTAssertEqual(1, fixture.delegate.addCrumbInvocations.count) + if let data = fixture.delegate.addCrumbInvocations.first?.data { + XCTAssertNil(data["level"], "Out-of-range battery level should be omitted") + } + } + func testBatteryUIDeviceNilNotification() { let currentDevice = MyUIDevice() diff --git a/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m index a1fadfe1d49..3f58bd70207 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashJSONCodec_Tests.m @@ -884,6 +884,84 @@ - (void)testSerializeDeserializeNANDouble XCTAssertTrue([[result objectAtIndex:0] isKindOfClass:[NSNull class]]); } +- (void)testSerializeDeserializePositiveInfinityFloat +{ + NSError *error = (NSError *)self; + NSString *expected = @"[1e999]"; + float infValue = INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithFloat:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); +} + +- (void)testSerializeDeserializeNegativeInfinityFloat +{ + NSError *error = (NSError *)self; + NSString *expected = @"[-1e999]"; + float infValue = -INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithFloat:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); + XCTAssertTrue([[result objectAtIndex:0] doubleValue] < 0, @"Should be negative infinity"); +} + +- (void)testSerializeDeserializePositiveInfinityDouble +{ + NSError *error = (NSError *)self; + NSString *expected = @"[1e999]"; + double infValue = (double)INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithDouble:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); +} + +- (void)testSerializeDeserializeNegativeInfinityDouble +{ + NSError *error = (NSError *)self; + NSString *expected = @"[-1e999]"; + double infValue = -(double)INFINITY; + id original = [NSArray arrayWithObjects:[NSNumber numberWithDouble:infValue], nil]; + + NSString *jsonString = toString([SentryCrashJSONCodec encode:original + options:SentryCrashJSONEncodeOptionSorted + error:&error]); + XCTAssertNotNil(jsonString, @""); + XCTAssertNil(error, @""); + XCTAssertEqualObjects(jsonString, expected, @""); + id result = [SentryCrashJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(result, @""); + XCTAssertNil(error, @""); + XCTAssertTrue(isinf([[result objectAtIndex:0] doubleValue]), @"Should decode back to infinity"); + XCTAssertTrue([[result objectAtIndex:0] doubleValue] < 0, @"Should be negative infinity"); +} + - (void)testSerializeDeserializeChar { NSError *error = (NSError *)self;