Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,6 @@ limitations under the License.

---

## FasterXML Jackson — ISO8601Utils (Apache 2.0)

**Source:** https://github.com/FasterXML/jackson-databind<br>
**License:** Apache License 2.0<br>
**Copyright:** Copyright (C) 2007-, Tatu Saloranta

### Scope

The Sentry Java SDK includes an adapted version of `ISO8601Utils` from the Jackson Databind library for ISO 8601 date/time parsing and formatting. The code resides in `io.sentry.vendor.gson.internal.bind.util.ISO8601Utils`.

```
Copyright (C) 2007-, Tatu Saloranta

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```

---

## Android Open Source Project — Base64 (Apache 2.0)

**Source:** https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Base64.java<br>
Expand Down
9 changes: 0 additions & 9 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -7998,15 +7998,6 @@ public class io/sentry/vendor/Base64 {
public static fun encodeToString ([BIII)Ljava/lang/String;
}

public class io/sentry/vendor/gson/internal/bind/util/ISO8601Utils {
public static final field TIMEZONE_UTC Ljava/util/TimeZone;
public fun <init> ()V
public static fun format (Ljava/util/Date;)Ljava/lang/String;
public static fun format (Ljava/util/Date;Z)Ljava/lang/String;
public static fun format (Ljava/util/Date;ZLjava/util/TimeZone;)Ljava/lang/String;
public static fun parse (Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;
}

public class io/sentry/vendor/gson/stream/JsonReader : java/io/Closeable {
public fun <init> (Ljava/io/Reader;)V
public fun beginArray ()V
Expand Down
5 changes: 3 additions & 2 deletions sentry/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ tasks.jacocoTestReport {
animalsniffer {
ignore =
listOf(
// We manually check on Android if it's available (API 26+).
"java.time.Instant"
// java.time is only available on Android API 26+. We check availability at runtime
// and fall back to a legacy implementation for older devices.
"java.time.*"
)
}

Expand Down
226 changes: 217 additions & 9 deletions sentry/src/main/java/io/sentry/DateUtils.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
package io.sentry;

import static io.sentry.vendor.gson.internal.bind.util.ISO8601Utils.TIMEZONE_UTC;

import io.sentry.vendor.gson.internal.bind.util.ISO8601Utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

/** Utilities to deal with dates */
@ApiStatus.Internal
public final class DateUtils {

private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone("UTC");

@VisibleForTesting static final boolean HAS_JAVA_TIME;

static {
boolean available;
try {
Class.forName("java.time.Instant");
available = true;
} catch (ClassNotFoundException e) {
available = false;
}
HAS_JAVA_TIME = available;
}

private DateUtils() {}

/**
Expand All @@ -39,9 +53,14 @@ private DateUtils() {}
public static @NotNull Date getDateTime(final @NotNull String timestamp)
throws IllegalArgumentException {
try {
return ISO8601Utils.parse(timestamp, new ParsePosition(0));
} catch (ParseException e) {
throw new IllegalArgumentException("timestamp is not ISO format " + timestamp);
if (HAS_JAVA_TIME) {
return Iso8601JavaTime.parse(timestamp);
}
return Iso8601Legacy.parse(timestamp);
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
throw new IllegalArgumentException("timestamp is not ISO format " + timestamp, e);
}
}

Expand All @@ -68,8 +87,12 @@ private DateUtils() {}
* @param date the UTC Date
* @return the UTC/ISO 8601 timestamp
*/
@SuppressWarnings("JavaUtilDate")
public static @NotNull String getTimestamp(final @NotNull Date date) {
return ISO8601Utils.format(date, true);
if (HAS_JAVA_TIME) {
return Iso8601JavaTime.format(date);
}
return Iso8601Legacy.format(date);
}

/**
Expand Down Expand Up @@ -169,4 +192,189 @@ public static long secondsToNanos(final @NotNull long seconds) {
public static @NotNull BigDecimal doubleToBigDecimal(final @NotNull Double value) {
return BigDecimal.valueOf(value).setScale(6, RoundingMode.DOWN);
}

// region java.time-based ISO 8601 (JVM and Android API 26+)

@SuppressWarnings("NewApi")
static final class Iso8601JavaTime {
private static final java.time.format.DateTimeFormatter FORMATTER =
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.withZone(java.time.ZoneOffset.UTC);

@SuppressWarnings("JavaUtilDate")
static @NotNull String format(final @NotNull Date date) {
return FORMATTER.format(java.time.Instant.ofEpochMilli(date.getTime()));
}

@SuppressWarnings("JavaUtilDate")
static @NotNull Date parse(final @NotNull String timestamp) {
try {
java.time.OffsetDateTime odt = java.time.OffsetDateTime.parse(timestamp);
return new Date(odt.toInstant().toEpochMilli());
} catch (java.time.format.DateTimeParseException e) {
try {
java.time.LocalDate localDate = java.time.LocalDate.parse(timestamp);
return new Date(
localDate.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli());
} catch (java.time.format.DateTimeParseException e2) {
throw new IllegalArgumentException("timestamp is not ISO format " + timestamp, e);
}
}
}
}

// endregion

// region Legacy ISO 8601 fallback (Android API < 26 without desugaring)

@SuppressWarnings({"MagicConstant", "JdkObsolete"})
static final class Iso8601Legacy {
static @NotNull String format(final @NotNull Date date) {
Calendar calendar = new GregorianCalendar(TIMEZONE_UTC, Locale.US);
calendar.setTime(date);
StringBuilder sb = new StringBuilder(24);
padInt(sb, calendar.get(Calendar.YEAR), 4);
sb.append('-');
padInt(sb, calendar.get(Calendar.MONTH) + 1, 2);
sb.append('-');
padInt(sb, calendar.get(Calendar.DAY_OF_MONTH), 2);
sb.append('T');
padInt(sb, calendar.get(Calendar.HOUR_OF_DAY), 2);
sb.append(':');
padInt(sb, calendar.get(Calendar.MINUTE), 2);
sb.append(':');
padInt(sb, calendar.get(Calendar.SECOND), 2);
sb.append('.');
padInt(sb, calendar.get(Calendar.MILLISECOND), 3);
sb.append('Z');
return sb.toString();
}

static @NotNull Date parse(final @NotNull String date) {
int offset = 0;
int year = parseInt(date, offset, offset += 4);
if (checkOffset(date, offset, '-')) offset++;
int month = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, '-')) offset++;
int day = parseInt(date, offset, offset += 2);

int hour = 0;
int minutes = 0;
int seconds = 0;
int milliseconds = 0;

if (date.length() <= offset) {
return new GregorianCalendar(year, month - 1, day).getTime();
}

if (checkOffset(date, offset, 'T')) {
offset++;
hour = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, ':')) offset++;
minutes = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, ':')) offset++;

if (offset < date.length()) {
char c = date.charAt(offset);
if (c != 'Z' && c != '+' && c != '-') {
seconds = parseInt(date, offset, offset += 2);
if (seconds > 59 && seconds < 63) seconds = 59;
if (checkOffset(date, offset, '.')) {
offset++;
int endOffset = offset;
while (endOffset < date.length() && Character.isDigit(date.charAt(endOffset))) {
endOffset++;
}
int parseEnd = Math.min(endOffset, offset + 3);
int fraction = parseInt(date, offset, parseEnd);
switch (parseEnd - offset) {
case 2:
milliseconds = fraction * 10;
break;
case 1:
milliseconds = fraction * 100;
break;
default:
milliseconds = fraction;
}
offset = endOffset;
}
}
}
}

if (date.length() <= offset) {
throw new IllegalArgumentException("No time zone indicator");
}

TimeZone timezone;
char tzIndicator = date.charAt(offset);
if (tzIndicator == 'Z') {
timezone = TIMEZONE_UTC;
} else if (tzIndicator == '+' || tzIndicator == '-') {
String tzOffset = date.substring(offset);
if (tzOffset.length() < 5) tzOffset = tzOffset + "00";
if ("+0000".equals(tzOffset) || "+00:00".equals(tzOffset)) {
timezone = TIMEZONE_UTC;
} else {
timezone = TimeZone.getTimeZone("GMT" + tzOffset);
}
} else {
throw new IllegalArgumentException("Invalid time zone indicator '" + tzIndicator + "'");
}

Calendar calendar = new GregorianCalendar(timezone);
calendar.setLenient(false);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minutes);
calendar.set(Calendar.SECOND, seconds);
calendar.set(Calendar.MILLISECOND, milliseconds);

return calendar.getTime();
}

private static boolean checkOffset(String value, int offset, char expected) {
return offset < value.length() && value.charAt(offset) == expected;
}

private static int parseInt(String value, int beginIndex, int endIndex) {
if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
throw new NumberFormatException(value);
}
int i = beginIndex;
int result = 0;
int digit;
if (i < endIndex) {
digit = Character.digit(value.charAt(i++), 10);
if (digit < 0) {
throw new NumberFormatException(
"Invalid number: " + value.substring(beginIndex, endIndex));
}
result = -digit;
}
while (i < endIndex) {
digit = Character.digit(value.charAt(i++), 10);
if (digit < 0) {
throw new NumberFormatException(
"Invalid number: " + value.substring(beginIndex, endIndex));
}
result *= 10;
result -= digit;
}
return -result;
}

private static void padInt(StringBuilder buffer, int value, int length) {
String strValue = Integer.toString(value);
for (int i = length - strValue.length(); i > 0; i--) {
buffer.append('0');
}
buffer.append(strValue);
}
}

// endregion
}
Loading
Loading