Skip to content

Commit c6be745

Browse files
authored
Merge pull request #2158 from bugsnag/PLAT-13769/off-by-one-error-device-date
Fix crash-time date calculation
2 parents 3a3c7ba + 7b95973 commit c6be745

File tree

5 files changed

+108
-7
lines changed

5 files changed

+108
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
* Excess threads (over `Configuration.getMaxReportedThreads`) are trimmed more reliably when the payload is modified before sending (in an `OnSendCallback` for example)
88
[#2148](https://github.com/bugsnag/bugsnag-android/pull/2148)
99

10+
* Fixed an error calculating the device time during NDK crashes
11+
[#2158](https://github.com/bugsnag/bugsnag-android/pull/2158)
12+
1013
## 6.12.0 (2025-02-18)
1114

1215
### Enhancements

bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ package com.bugsnag.android.ndk
33
import org.junit.After
44
import org.junit.Assert.assertEquals
55
import org.junit.Before
6+
import org.junit.Ignore
67
import org.junit.Test
78
import java.io.File
9+
import java.text.SimpleDateFormat
10+
import java.util.Calendar
11+
import java.util.Date
12+
import java.util.GregorianCalendar
13+
import java.util.Locale
14+
import java.util.TimeZone
815

916
class NativeJsonSerializeTest {
1017

@@ -16,6 +23,8 @@ class NativeJsonSerializeTest {
1623
}
1724

1825
private val path = File(System.getProperty("java.io.tmpdir"), this::class.simpleName!!)
26+
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
27+
.apply { timeZone = TimeZone.getTimeZone("UTC") }
1928

2029
@Before
2130
fun setupTmpdir() {
@@ -27,13 +36,84 @@ class NativeJsonSerializeTest {
2736
path.deleteRecursively()
2837
}
2938

30-
external fun run(outputDir: String): Int
39+
external fun run(outputDir: String, timestamp: Long): Int
3140

3241
@Test
33-
fun testPassesNativeSuite() {
34-
verifyNativeRun(run(path.absolutePath))
42+
fun testPassesNativeSuiteEpoch() {
43+
verifyNativeRun(run(path.absolutePath, 7609))
3544
val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!!
3645
val expected = loadJson("event_serialization.json")
3746
assertEquals(expected, jsonFile.readText())
3847
}
48+
49+
@Test
50+
fun testRegression2024() {
51+
val timestamp = GregorianCalendar(2024, 11, 30, 16, 0, 0).timeInMillis
52+
val datestamp = dateFormat.format(Date(timestamp))
53+
54+
verifyNativeRun(run(path.absolutePath, timestamp / 1000L))
55+
val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!!
56+
val expected = loadJson("event_serialization.json")
57+
.replace("\"1970-01-01T02:06:49Z\"", "\"${datestamp}\"")
58+
assertEquals(expected, jsonFile.readText())
59+
}
60+
61+
@Test
62+
fun testPassesNativeSuite2024() {
63+
val timestamp = GregorianCalendar(2024, 0, 1).timeInMillis
64+
val datestamp = dateFormat.format(Date(timestamp))
65+
66+
verifyNativeRun(run(path.absolutePath, timestamp / 1000L))
67+
val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!!
68+
val expected = loadJson("event_serialization.json")
69+
.replace("\"1970-01-01T02:06:49Z\"", "\"${datestamp}\"")
70+
assertEquals(expected, jsonFile.readText())
71+
}
72+
73+
@Test
74+
fun testPassesNativeSuite2025() {
75+
val timestamp = GregorianCalendar(2025, 1, 1).timeInMillis
76+
val datestamp = dateFormat.format(Date(timestamp))
77+
78+
verifyNativeRun(run(path.absolutePath, timestamp / 1000L))
79+
val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!!
80+
val expected = loadJson("event_serialization.json")
81+
.replace("\"1970-01-01T02:06:49Z\"", "\"${datestamp}\"")
82+
assertEquals(expected, jsonFile.readText())
83+
}
84+
85+
@Test
86+
fun testPassesNativeSuiteToday() {
87+
val now = System.currentTimeMillis()
88+
val datestamp = dateFormat.format(Date(now))
89+
90+
verifyNativeRun(run(path.absolutePath, now / 1000L))
91+
val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!!
92+
val expected = loadJson("event_serialization.json")
93+
.replace("\"1970-01-01T02:06:49Z\"", "\"${datestamp}\"")
94+
assertEquals(expected, jsonFile.readText())
95+
}
96+
97+
@Test
98+
@Ignore("useful when working on the date formatting code")
99+
fun testDecadesOfDates() {
100+
val calendar = Calendar.getInstance().apply { add(Calendar.YEAR, -10) }
101+
val end = Calendar.getInstance().apply { add(Calendar.YEAR, 10) }
102+
103+
while (calendar < end) {
104+
val instant = calendar.timeInMillis
105+
val datestamp = dateFormat.format(Date(instant))
106+
107+
verifyNativeRun(run(path.absolutePath, instant / 1000L))
108+
val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!!
109+
val expected = loadJson("event_serialization.json")
110+
.replace("\"1970-01-01T02:06:49Z\"", "\"${datestamp}\"")
111+
assertEquals(expected, jsonFile.readText())
112+
113+
// move the date along 6 hours at a time
114+
calendar.add(Calendar.HOUR, 6)
115+
116+
jsonFile.delete()
117+
}
118+
}
39119
}

bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
#include "BSG_KSCrashStringConversion.h"
10+
#include "utils/logger.h"
1011
#include <math.h>
1112
#include <memory.h>
1213
#include <time.h>
@@ -218,9 +219,19 @@ static void safe_gmtime_r(time_t time, struct tm *out) {
218219
days -= quotient * days_per_4years;
219220
years += quotient * 4;
220221

221-
quotient = days / 365;
222-
days -= quotient * 365;
223-
years += quotient;
222+
while (days >= 365) {
223+
if (years % 4 == 0 && (years % 100 != 0 || years % 400 == 0)) {
224+
if (days >= 366) {
225+
days -= 366;
226+
years += 1;
227+
} else {
228+
break;
229+
}
230+
} else {
231+
days -= 365;
232+
years += 1;
233+
}
234+
}
224235

225236
out->tm_year = years - 1900;
226237
out->tm_yday = days;

bugsnag-plugin-android-ndk/src/test/cpp/main.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Java_com_bugsnag_android_ndk_NativeStringTest_run(JNIEnv *_env, jobject _this) {
5959
extern bool bsg_event_write(bsg_environment *env);
6060

6161
JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeJsonSerializeTest_run(
62-
JNIEnv *_env, jobject _this, jstring _dir) {
62+
JNIEnv *_env, jobject _this, jstring _dir, jlong timestamp) {
6363

6464
const char *dir = (*_env)->GetStringUTFChars(_env, _dir, NULL);
6565
if (dir == NULL) {
@@ -71,12 +71,15 @@ JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeJsonSerializeTest_run(
7171
bugsnag_event *event = init_event();
7272
memcpy(&env.next_event, event, sizeof(bugsnag_event));
7373

74+
env.next_event.device.time = (time_t) timestamp;
7475
env.event_path = strdup(dir);
76+
env.static_json_data = NULL;
7577
strcpy(env.event_uuid, "test-uuid");
7678

7779
bsg_event_write(&env);
7880

7981
free(event);
82+
free(env.event_path);
8083

8184
(*_env)->ReleaseStringUTFChars(_env, _dir, dir);
8285

features/support/env.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060
skip_this_scenario("Skipping scenario") if Maze.config.os_version < 5
6161
end
6262

63+
Before('@skip_android_14') do |scenario|
64+
skip_this_scenario("Skipping scenario") if Maze.config.os_version.floor == 14
65+
end
66+
6367
Before('@skip_android_13') do |scenario|
6468
skip_this_scenario("Skipping scenario") if Maze.config.os_version.floor == 13
6569
end

0 commit comments

Comments
 (0)