Skip to content

Commit ff355de

Browse files
ilya-gdkhalanskyjb
authored andcommitted
Use externalizable replacement for serializable entities
1 parent 71f85c8 commit ff355de

File tree

7 files changed

+125
-86
lines changed

7 files changed

+125
-86
lines changed

core/jvm/src/Instant.kt

+1-17
Original file line numberDiff line numberDiff line change
@@ -99,25 +99,9 @@ public actual class Instant internal constructor(
9999

100100
internal actual val MIN: Instant = Instant(jtInstant.MIN)
101101
internal actual val MAX: Instant = Instant(jtInstant.MAX)
102-
103-
private const val serialVersionUID: Long = 1L
104-
}
105-
106-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
107-
oStream.defaultWriteObject()
108-
oStream.writeObject(value.toString())
109102
}
110103

111-
private fun readObject(iStream: java.io.ObjectInputStream) {
112-
iStream.defaultReadObject()
113-
val field = this::class.java.getDeclaredField(::value.name)
114-
field.isAccessible = true
115-
field.set(this, parse(iStream.readObject() as String).value)
116-
}
117-
118-
private fun readObjectNoData() {
119-
throw java.io.InvalidObjectException("Stream data required")
120-
}
104+
private fun writeReplace(): Any = SerializedValue(SerializedValue.INSTANT_TAG, this)
121105
}
122106

123107
private fun Instant.atZone(zone: TimeZone): java.time.ZonedDateTime = try {

core/jvm/src/LocalDate.kt

+1-17
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ public actual class LocalDate internal constructor(
5252
@Suppress("FunctionName")
5353
public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate> =
5454
LocalDateFormat.build(block)
55-
56-
private const val serialVersionUID: Long = 1L
5755
}
5856

5957
public actual object Formats {
@@ -93,21 +91,7 @@ public actual class LocalDate internal constructor(
9391
@JvmName("toEpochDays")
9492
internal fun toEpochDaysJvm(): Int = value.toEpochDay().clampToInt()
9593

96-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
97-
oStream.defaultWriteObject()
98-
oStream.writeObject(value.toString())
99-
}
100-
101-
private fun readObject(iStream: java.io.ObjectInputStream) {
102-
iStream.defaultReadObject()
103-
val field = this::class.java.getDeclaredField(::value.name)
104-
field.isAccessible = true
105-
field.set(this, jtLocalDate.parse(iStream.readObject() as String))
106-
}
107-
108-
private fun readObjectNoData() {
109-
throw java.io.InvalidObjectException("Stream data required")
110-
}
94+
private fun writeReplace(): Any = SerializedValue(SerializedValue.DATE_TAG, this)
11195
}
11296

11397
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))

core/jvm/src/LocalDateTime.kt

+2-17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package kotlinx.datetime
77

88
import kotlinx.datetime.format.*
99
import kotlinx.datetime.internal.removeLeadingZerosFromLongYearFormLocalDateTime
10+
import kotlinx.datetime.internal.SerializedValue
1011
import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer
1112
import kotlinx.serialization.Serializable
1213
import java.time.DateTimeException
@@ -82,27 +83,11 @@ public actual class LocalDateTime internal constructor(
8283
@Suppress("FunctionName")
8384
public actual fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat<LocalDateTime> =
8485
LocalDateTimeFormat.build(builder)
85-
86-
private const val serialVersionUID: Long = 1L
8786
}
8887

8988
public actual object Formats {
9089
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
9190
}
9291

93-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
94-
oStream.defaultWriteObject()
95-
oStream.writeObject(value.toString())
96-
}
97-
98-
private fun readObject(iStream: java.io.ObjectInputStream) {
99-
iStream.defaultReadObject()
100-
val field = this::class.java.getDeclaredField(::value.name)
101-
field.isAccessible = true
102-
field.set(this, jtLocalDateTime.parse(iStream.readObject() as String))
103-
}
104-
105-
private fun readObjectNoData() {
106-
throw java.io.InvalidObjectException("Stream data required")
107-
}
92+
private fun writeReplace(): Any = SerializedValue(SerializedValue.DATE_TIME_TAG, this)
10893
}

core/jvm/src/LocalTime.kt

+1-17
Original file line numberDiff line numberDiff line change
@@ -85,28 +85,12 @@ public actual class LocalTime internal constructor(
8585
@Suppress("FunctionName")
8686
public actual fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat<LocalTime> =
8787
LocalTimeFormat.build(builder)
88-
89-
private const val serialVersionUID: Long = 1L
9088
}
9189

9290
public actual object Formats {
9391
public actual val ISO: DateTimeFormat<LocalTime> get() = ISO_TIME
9492

9593
}
9694

97-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
98-
oStream.defaultWriteObject()
99-
oStream.writeObject(value.toString())
100-
}
101-
102-
private fun readObject(iStream: java.io.ObjectInputStream) {
103-
iStream.defaultReadObject()
104-
val field = this::class.java.getDeclaredField(::value.name)
105-
field.isAccessible = true
106-
field.set(this, jtLocalTime.parse(iStream.readObject() as String))
107-
}
108-
109-
private fun readObjectNoData() {
110-
throw java.io.InvalidObjectException("Stream data required")
111-
}
95+
private fun writeReplace(): Any = SerializedValue(SerializedValue.TIME_TAG, this)
11296
}

core/jvm/src/UtcOffsetJvm.kt

+2-15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime
77

88
import kotlinx.datetime.format.*
9+
import kotlinx.datetime.internal.SerializedValue
910
import kotlinx.datetime.serializers.UtcOffsetSerializer
1011
import kotlinx.serialization.Serializable
1112
import java.time.DateTimeException
@@ -47,21 +48,7 @@ public actual class UtcOffset(
4748
public actual val FOUR_DIGITS: DateTimeFormat<UtcOffset> get() = FOUR_DIGIT_OFFSET
4849
}
4950

50-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
51-
oStream.defaultWriteObject()
52-
oStream.writeObject(zoneOffset.toString())
53-
}
54-
55-
private fun readObject(iStream: java.io.ObjectInputStream) {
56-
iStream.defaultReadObject()
57-
val field = this::class.java.getDeclaredField(::zoneOffset.name)
58-
field.isAccessible = true
59-
field.set(this, ZoneOffset.of(iStream.readObject() as String))
60-
}
61-
62-
private fun readObjectNoData() {
63-
throw java.io.InvalidObjectException("Stream data required")
64-
}
51+
private fun writeReplace(): Any = SerializedValue(SerializedValue.UTC_OFFSET_TAG, this)
6552
}
6653

6754
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime.internal
7+
8+
import kotlinx.datetime.*
9+
import java.io.*
10+
11+
internal class SerializedValue(var typeTag: Int, var value: Any?) : Externalizable {
12+
constructor() : this(0, null)
13+
14+
override fun writeExternal(out: ObjectOutput) {
15+
out.writeByte(typeTag)
16+
val value = this.value
17+
when (typeTag) {
18+
INSTANT_TAG -> {
19+
value as Instant
20+
out.writeLong(value.epochSeconds)
21+
out.writeInt(value.nanosecondsOfSecond)
22+
}
23+
DATE_TAG -> {
24+
value as LocalDate
25+
out.writeLong(value.value.toEpochDay())
26+
}
27+
TIME_TAG -> {
28+
value as LocalTime
29+
out.writeLong(value.toNanosecondOfDay())
30+
}
31+
DATE_TIME_TAG -> {
32+
value as LocalDateTime
33+
out.writeLong(value.date.value.toEpochDay())
34+
out.writeLong(value.time.toNanosecondOfDay())
35+
}
36+
UTC_OFFSET_TAG -> {
37+
value as UtcOffset
38+
out.writeInt(value.totalSeconds)
39+
}
40+
else -> throw IllegalStateException("Unknown type tag: $typeTag for value: $value")
41+
}
42+
}
43+
44+
override fun readExternal(`in`: ObjectInput) {
45+
typeTag = `in`.readByte().toInt()
46+
value = when (typeTag) {
47+
INSTANT_TAG ->
48+
Instant.fromEpochSeconds(`in`.readLong(), `in`.readInt())
49+
DATE_TAG ->
50+
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong()))
51+
TIME_TAG ->
52+
LocalTime.fromNanosecondOfDay(`in`.readLong())
53+
DATE_TIME_TAG ->
54+
LocalDateTime(
55+
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong())),
56+
LocalTime.fromNanosecondOfDay(`in`.readLong())
57+
)
58+
UTC_OFFSET_TAG ->
59+
UtcOffset(seconds = `in`.readInt())
60+
else -> throw IOException("Unknown type tag: $typeTag")
61+
}
62+
}
63+
64+
private fun readResolve(): Any = value!!
65+
66+
companion object {
67+
private const val serialVersionUID: Long = 0L
68+
const val INSTANT_TAG = 1
69+
const val DATE_TAG = 2
70+
const val TIME_TAG = 3
71+
const val DATE_TIME_TAG = 4
72+
const val UTC_OFFSET_TAG = 10
73+
}
74+
}

core/jvm/test/JvmSerializationTest.kt

+44-3
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,41 @@ class JvmSerializationTest {
1313
@Test
1414
fun serializeInstant() {
1515
roundTripSerialization(Instant.fromEpochSeconds(1234567890, 123456789))
16+
roundTripSerialization(Instant.MIN)
17+
roundTripSerialization(Instant.MAX)
18+
expectedDeserialization(Instant.parse("+150000-04-30T12:30:25.555998Z"), "0d010000043fa44d82612123db30")
1619
}
1720

1821
@Test
1922
fun serializeLocalTime() {
2023
roundTripSerialization(LocalTime(12, 34, 56, 789))
24+
roundTripSerialization(LocalTime.MIN)
25+
roundTripSerialization(LocalTime.MAX)
26+
expectedDeserialization(LocalTime(23, 59, 15, 995_003_220), "090300004e8a52680954")
27+
}
28+
29+
@Test
30+
fun serializeLocalDate() {
31+
roundTripSerialization(LocalDate(2022, 1, 23))
32+
roundTripSerialization(LocalDate.MIN)
33+
roundTripSerialization(LocalDate.MAX)
34+
expectedDeserialization(LocalDate(2024, 8, 12), "09020000000000004deb")
2135
}
2236

2337
@Test
2438
fun serializeLocalDateTime() {
2539
roundTripSerialization(LocalDateTime(2022, 1, 23, 21, 35, 53, 125_123_612))
40+
roundTripSerialization(LocalDateTime.MIN)
41+
roundTripSerialization(LocalDateTime.MAX)
42+
expectedDeserialization(LocalDateTime(2024, 8, 12, 10, 15, 0, 997_665_331), "11040000000000004deb0000218faedb9233")
2643
}
2744

2845
@Test
2946
fun serializeUtcOffset() {
3047
roundTripSerialization(UtcOffset(hours = 3, minutes = 30, seconds = 15))
48+
roundTripSerialization(UtcOffset(java.time.ZoneOffset.MIN))
49+
roundTripSerialization(UtcOffset(java.time.ZoneOffset.MAX))
50+
expectedDeserialization(UtcOffset.parse("-04:15:30"), "050affffc41e")
3151
}
3252

3353
@Test
@@ -37,14 +57,35 @@ class JvmSerializationTest {
3757
}
3858
}
3959

40-
private fun <T> roundTripSerialization(value: T) {
60+
private fun serialize(value: Any?): ByteArray {
4161
val bos = ByteArrayOutputStream()
4262
val oos = ObjectOutputStream(bos)
4363
oos.writeObject(value)
44-
val serialized = bos.toByteArray()
64+
return bos.toByteArray()
65+
}
66+
67+
private fun deserialize(serialized: ByteArray): Any? {
4568
val bis = ByteArrayInputStream(serialized)
4669
ObjectInputStream(bis).use { ois ->
47-
assertEquals(value, ois.readObject())
70+
return ois.readObject()
71+
}
72+
}
73+
74+
private fun <T> roundTripSerialization(value: T) {
75+
val serialized = serialize(value)
76+
val deserialized = deserialize(serialized)
77+
assertEquals(value, deserialized)
78+
}
79+
80+
@OptIn(ExperimentalStdlibApi::class)
81+
private fun expectedDeserialization(expected: Any, blockData: String) {
82+
val serialized = "aced0005737200296b6f746c696e782e6461746574696d652e696e7465726e616c2e53657269616c697a656456616c756500000000000000000c0000787077${blockData}78"
83+
val hexFormat = HexFormat { bytes.byteSeparator = "" }
84+
85+
val deserialized = deserialize(serialized.hexToByteArray(hexFormat))
86+
if (expected != deserialized) {
87+
assertEquals(expected, deserialized, "Golden serial form: $serialized\nActual serial form: ${serialize(expected).toHexString(hexFormat)}")
4888
}
4989
}
90+
5091
}

0 commit comments

Comments
 (0)