Replace ZonedDateTime with Instant (#6554)

This commit is contained in:
David Perez
2026-02-20 13:02:25 -06:00
committed by GitHub
parent 92664b6752
commit c6b4c490ca
112 changed files with 566 additions and 680 deletions

View File

@@ -6,28 +6,31 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
/**
* Used to serialize and deserialize [ZonedDateTime].
* Used to serialize and deserialize [Instant].
*/
class ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
class InstantSerializer : KSerializer<Instant> {
private val dateTimeFormatterDeserialization = DateTimeFormatter
.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.][:][SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]XXX")
private val dateTimeFormatterSerialization =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX")
private val dateTimeFormatterSerialization: DateTimeFormatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX")
.withZone(ZoneOffset.UTC)
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor(serialName = "ZonedDateTime", kind = PrimitiveKind.STRING)
get() = PrimitiveSerialDescriptor(serialName = "Instant", kind = PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): ZonedDateTime =
decoder.decodeString().let { dateString ->
ZonedDateTime.parse(dateString, dateTimeFormatterDeserialization)
}
override fun deserialize(decoder: Decoder): Instant =
ZonedDateTime
.parse(decoder.decodeString(), dateTimeFormatterDeserialization)
.toInstant()
override fun serialize(encoder: Encoder, value: ZonedDateTime) {
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(dateTimeFormatterSerialization.format(value))
}
}

View File

@@ -1,6 +1,6 @@
package com.bitwarden.core.di
import com.bitwarden.core.data.serializer.ZonedDateTimeSerializer
import com.bitwarden.core.data.serializer.InstantSerializer
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -30,7 +30,7 @@ object CoreModule {
// We allow for nullable values to have keys missing in the JSON response.
explicitNulls = false
serializersModule = SerializersModule {
contextual(ZonedDateTimeSerializer())
contextual(InstantSerializer())
}
// Respect model default property values.

View File

@@ -0,0 +1,16 @@
package com.bitwarden.core.util
import java.time.Clock
import java.time.Duration
import java.time.Instant
/**
* Returns a [Boolean] indicating whether this [Instant] is five or more minutes old.
*/
@Suppress("MagicNumber")
fun Instant.isOverFiveMinutesOld(
clock: Clock = Clock.systemDefaultZone(),
): Boolean =
Duration
.between(this, clock.instant())
.toMinutes() > 5

View File

@@ -1,8 +1,6 @@
package com.bitwarden.core.util
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
private const val NANOS_PER_TICK = 100L
private const val TICKS_PER_SECOND = 1000000000L / NANOS_PER_TICK
@@ -13,32 +11,31 @@ private const val TICKS_PER_SECOND = 1000000000L / NANOS_PER_TICK
private const val YEAR_OFFSET = -62135596800L
/**
* Returns the [ZonedDateTime] of the binary [Long] [value]. This is needed to remain consistent
* Returns the [Instant] of the binary [Long] [value]. This is needed to remain consistent
* with how `DateTime`s were stored when using C#.
*
* This functionality is based on the https://stackoverflow.com/questions/65315060/how-to-convert-net-datetime-tobinary-to-java-date
*/
@Suppress("MagicNumber")
fun getZoneDateTimeFromBinaryLong(value: Long): ZonedDateTime {
fun getInstantFromBinaryLong(value: Long): Instant {
// Shift the bits to eliminate the "Kind" property since we know it was stored as UTC and leave
// us with ticks
val ticks = value and (1L shl 62) - 1
val instant = Instant.ofEpochSecond(
return Instant.ofEpochSecond(
ticks / TICKS_PER_SECOND + YEAR_OFFSET,
ticks % TICKS_PER_SECOND * NANOS_PER_TICK,
)
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)
}
/**
* Returns the [ZonedDateTime] [value] converted to a binary [Long]. This is needed to remain
* Returns the [Instant] [value] converted to a binary [Long]. This is needed to remain
* consistent with how `DateTime`s were stored when using C#.
*
* This functionality is based on the https://stackoverflow.com/questions/65315060/how-to-convert-net-datetime-tobinary-to-java-date
*/
@Suppress("MagicNumber")
fun getBinaryLongFromZoneDateTime(value: ZonedDateTime): Long {
fun getBinaryLongFromInstant(value: Instant): Long {
val nanoAdjustment = value.nano / NANOS_PER_TICK
val ticks = (value.toEpochSecond() - YEAR_OFFSET) * TICKS_PER_SECOND + nanoAdjustment
val ticks = (value.epochSecond - YEAR_OFFSET) * TICKS_PER_SECOND + nanoAdjustment
return 1L shl 62 or ticks
}

View File

@@ -1,16 +0,0 @@
package com.bitwarden.core.util
import java.time.Clock
import java.time.Duration
import java.time.ZonedDateTime
/**
* Returns a [Boolean] indicating whether this [ZonedDateTime] is five or more minutes old.
*/
@Suppress("MagicNumber")
fun ZonedDateTime.isOverFiveMinutesOld(
clock: Clock = Clock.systemDefaultZone(),
): Boolean =
Duration
.between(this.toInstant(), clock.instant())
.toMinutes() > 5

View File

@@ -0,0 +1,70 @@
package com.bitwarden.core.data.serializer
import com.bitwarden.core.di.CoreModule
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.encodeToJsonElement
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.Instant
class InstantSerializerTest {
private val json = CoreModule.providesJson()
@Test
fun `properly deserializes raw JSON to Instant`() {
assertEquals(
InstantData(
dataAsInstant = Instant.ofEpochSecond(1_696_612_948L, 440_000_000L),
),
json.decodeFromString<InstantData>(
"""
{
"dataAsInstant": "2023-10-06T17:22:28.44Z"
}
""",
),
)
}
@Test
fun `properly deserializes raw JSON with nano seconds to Instant`() {
assertEquals(
InstantData(
dataAsInstant = Instant.ofEpochSecond(1_690_906_383L, 502_391_000L),
),
json.decodeFromString<InstantData>(
"""
{
"dataAsInstant": "2023-08-01T16:13:03.502391Z"
}
""",
),
)
}
@Test
fun `properly serializes external model back to raw JSON`() {
assertEquals(
json.parseToJsonElement(
"""
{
"dataAsInstant": "2023-10-06T17:22:28.440Z"
}
""",
),
json.encodeToJsonElement(
InstantData(
dataAsInstant = Instant.ofEpochSecond(1_696_612_948L, 440_000_000L),
),
),
)
}
}
@Serializable
private data class InstantData(
@Serializable(InstantSerializer::class)
@SerialName("dataAsInstant")
val dataAsInstant: Instant,
)

View File

@@ -1,99 +1 @@
package com.bitwarden.core.data.serializer
import com.bitwarden.core.di.CoreModule
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.encodeToJsonElement
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.ZonedDateTime
class ZonedDateTimeSerializerTest {
private val json = CoreModule.providesJson()
@Test
fun `properly deserializes raw JSON to ZonedDateTime`() {
assertEquals(
ZonedDateTimeData(
dataAsZonedDateTime = ZonedDateTime.of(
2023,
10,
6,
17,
22,
28,
440000000,
ZoneOffset.UTC,
),
),
json.decodeFromString<ZonedDateTimeData>(
"""
{
"dataAsZonedDateTime": "2023-10-06T17:22:28.44Z"
}
""",
),
)
}
@Test
fun `properly deserializes raw JSON with nano seconds to ZonedDateTime`() {
assertEquals(
ZonedDateTimeData(
dataAsZonedDateTime = ZonedDateTime.of(
2023,
8,
1,
16,
13,
3,
502391000,
ZoneOffset.UTC,
),
),
json.decodeFromString<ZonedDateTimeData>(
"""
{
"dataAsZonedDateTime": "2023-08-01T16:13:03.502391Z"
}
""",
),
)
}
@Test
fun `properly serializes external model back to raw JSON`() {
assertEquals(
json.parseToJsonElement(
"""
{
"dataAsZonedDateTime": "2023-10-06T17:22:28.440Z"
}
""",
),
json.encodeToJsonElement(
ZonedDateTimeData(
dataAsZonedDateTime = ZonedDateTime.of(
2023,
10,
6,
17,
22,
28,
440000000,
ZoneId.of("UTC"),
),
),
),
)
}
}
@Serializable
private data class ZonedDateTimeData(
@Serializable(ZonedDateTimeSerializer::class)
@SerialName("dataAsZonedDateTime")
val dataAsZonedDateTime: ZonedDateTime,
)

View File

@@ -6,9 +6,8 @@ import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
class ZonedDateTimeExtensionsTest {
class InstantExtensionsTest {
private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
@@ -17,25 +16,25 @@ class ZonedDateTimeExtensionsTest {
@Test
fun `isOverFiveMinutesOld returns true when time is old`() {
val time = ZonedDateTime.parse("2022-09-13T00:00Z")
val time = Instant.parse("2022-09-13T00:00:00Z")
assertTrue(time.isOverFiveMinutesOld(fixedClock))
}
@Test
fun `isOverFiveMinutesOld returns false when time is now`() {
val time = ZonedDateTime.parse("2023-10-27T11:55:00Z")
val time = Instant.parse("2023-10-27T11:55:00Z")
assertFalse(time.isOverFiveMinutesOld(fixedClock))
}
@Test
fun `isOverFiveMinutesOld returns false when time is now minus 5 minutes`() {
val time = ZonedDateTime.now(fixedClock).minusMinutes(5)
val time = fixedClock.instant().minusSeconds(300)
assertFalse(time.isOverFiveMinutesOld(fixedClock))
}
@Test
fun `isOverFiveMinutesOld returns true when time is now minus 6 minutes`() {
val time = ZonedDateTime.now(fixedClock).minusMinutes(6)
val time = fixedClock.instant().minusSeconds(360)
assertTrue(time.isOverFiveMinutesOld(fixedClock))
}
}

View File

@@ -0,0 +1,25 @@
package com.bitwarden.core.util
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.Instant
class InstantUtilsTest {
@Test
fun `getInstantFromBinaryLong should correctly convert a Long to an Instant`() {
val binaryLong = 5250087787086431044L
val expectedInstant = Instant.parse("2024-01-06T22:27:45.904314Z")
assertEquals(expectedInstant, getInstantFromBinaryLong(binaryLong))
val a = getInstantFromBinaryLong(binaryLong)
val b = getBinaryLongFromInstant(a)
assertEquals(binaryLong, b)
}
@Test
fun `getBinaryLongFromInstant should correctly convert an Instant to a Long`() {
val instant = Instant.parse("2024-01-06T22:27:45.904314Z")
val expectedBinaryLong = 5250087787086431044L
assertEquals(expectedBinaryLong, getBinaryLongFromInstant(instant))
}
}

View File

@@ -1,25 +0,0 @@
package com.bitwarden.core.util
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
class ZonedDateTimeUtilsTest {
@Test
fun `getZoneDateTimeFromBinaryLong should correctly convert a Long to a ZonedDateTime`() {
val binaryLong = 5250087787086431044L
val expectedDateTime = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
assertEquals(expectedDateTime, getZoneDateTimeFromBinaryLong(binaryLong))
val a = getZoneDateTimeFromBinaryLong(binaryLong)
val b = getBinaryLongFromZoneDateTime(a)
assertEquals(binaryLong, b)
}
@Test
fun `getBinaryLongFromZoneDateTime should correctly convert a ZonedDateTime to a Long`() {
val dateTime = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
val expectedBinaryLong = 5250087787086431044L
assertEquals(expectedBinaryLong, getBinaryLongFromZoneDateTime(dateTime))
}
}