Skip to content

Commit ee58484

Browse files
committed
Validate Timestamp values are within spec's range
1 parent 1ae59b7 commit ee58484

File tree

4 files changed

+68
-6
lines changed

4 files changed

+68
-6
lines changed

wire-protoc-compatibility-tests/src/test/java/com/squareup/wire/Proto3WireProtocCompatibilityTests.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,14 +518,14 @@ class Proto3WireProtocCompatibilityTests {
518518
val googleMessage = PizzaOuterClass.PizzaDelivery.newBuilder()
519519
.setOrderedAt(
520520
Timestamp.newBuilder()
521-
.setSeconds(-631152000000L) // 1950-01-01T00:00:00.250Z.
521+
.setSeconds(-631152000L) // 1950-01-01T00:00:00.250Z.
522522
.setNanos(250_000_000)
523523
.build(),
524524
)
525525
.build()
526526

527527
val wireMessage = PizzaDeliveryK(
528-
ordered_at = ofEpochSecond(-631152000000L, 250_000_000L),
528+
ordered_at = ofEpochSecond(-631152000L, 250_000_000L),
529529
)
530530

531531
val googleMessageBytes = googleMessage.toByteArray()

wire-runtime/src/commonMain/kotlin/com/squareup/wire/Instant.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ expect class Instant {
2727
*
2828
* For example, this value will be -1 for the instant 1969-12-31T23:59:59Z, and 1 for the instant
2929
* 1970-01-01T00:00:01Z.
30+
*
31+
* It must be between -62135596800 and 253402300799 inclusive (which corresponds to
32+
* 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z).
3033
*/
3134
fun getEpochSecond(): Long
3235

wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,32 +1277,49 @@ internal fun commonDuration(): ProtoAdapter<Duration> = object : ProtoAdapter<Du
12771277
}
12781278
}
12791279

1280+
// Protobuf Timestamp valid range: 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z inclusive.
1281+
private const val TIMESTAMP_SECONDS_MIN = -62135596800L
1282+
private const val TIMESTAMP_SECONDS_MAX = 253402300799L
1283+
1284+
@Suppress("NOTHING_TO_INLINE")
1285+
private inline fun checkTimestampRange(seconds: Long, nanos: Int) {
1286+
require(seconds in TIMESTAMP_SECONDS_MIN..TIMESTAMP_SECONDS_MAX) {
1287+
"Timestamp seconds ($seconds) must be in range [$TIMESTAMP_SECONDS_MIN, $TIMESTAMP_SECONDS_MAX]"
1288+
}
1289+
require(nanos in 0..999_999_999) {
1290+
"Timestamp nanos ($nanos) must be in range [0, 999999999]"
1291+
}
1292+
}
1293+
12801294
internal fun commonInstant(): ProtoAdapter<Instant> = object : ProtoAdapter<Instant>(
12811295
LENGTH_DELIMITED,
12821296
Instant::class,
12831297
"type.googleapis.com/google.protobuf.Timestamp",
12841298
Syntax.PROTO_3,
12851299
) {
12861300
override fun encodedSize(value: Instant): Int {
1287-
var result = 0
12881301
val seconds = value.getEpochSecond()
1289-
if (seconds != 0L) result += INT64.encodedSizeWithTag(1, seconds)
12901302
val nanos = value.getNano()
1303+
checkTimestampRange(seconds, nanos)
1304+
var result = 0
1305+
if (seconds != 0L) result += INT64.encodedSizeWithTag(1, seconds)
12911306
if (nanos != 0) result += INT32.encodedSizeWithTag(2, nanos)
12921307
return result
12931308
}
12941309

12951310
override fun encode(writer: ProtoWriter, value: Instant) {
12961311
val seconds = value.getEpochSecond()
1297-
if (seconds != 0L) INT64.encodeWithTag(writer, 1, seconds)
12981312
val nanos = value.getNano()
1313+
checkTimestampRange(seconds, nanos)
1314+
if (seconds != 0L) INT64.encodeWithTag(writer, 1, seconds)
12991315
if (nanos != 0) INT32.encodeWithTag(writer, 2, nanos)
13001316
}
13011317

13021318
override fun encode(writer: ReverseProtoWriter, value: Instant) {
1319+
val seconds = value.getEpochSecond()
13031320
val nanos = value.getNano()
1321+
checkTimestampRange(seconds, nanos)
13041322
if (nanos != 0) INT32.encodeWithTag(writer, 2, nanos)
1305-
val seconds = value.getEpochSecond()
13061323
if (seconds != 0L) INT64.encodeWithTag(writer, 1, seconds)
13071324
}
13081325

wire-runtime/src/commonTest/kotlin/com/squareup/wire/ProtoAdapterTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package com.squareup.wire
1717

1818
import assertk.assertFailure
19+
import assertk.assertThat
20+
import assertk.assertions.isEqualTo
1921
import assertk.assertions.isInstanceOf
2022
import kotlin.test.Test
2123

@@ -44,4 +46,44 @@ class ProtoAdapterTest {
4446
ProtoAdapter.BOOL.asPacked().asRepeated()
4547
}.isInstanceOf<UnsupportedOperationException>()
4648
}
49+
50+
@Test fun instantEncodeValidMinBoundary() {
51+
// 0001-01-01T00:00:00Z.
52+
val instant = ofEpochSecond(-62135596800L, 0L)
53+
val bytes = ProtoAdapter.INSTANT.encode(instant)
54+
assertThat(ProtoAdapter.INSTANT.decode(bytes).getEpochSecond()).isEqualTo(-62135596800L)
55+
}
56+
57+
@Test fun instantEncodeValidMaxBoundary() {
58+
// 9999-12-31T23:59:59Z.
59+
val instant = ofEpochSecond(253402300799L, 999_999_999L)
60+
val bytes = ProtoAdapter.INSTANT.encode(instant)
61+
val decoded = ProtoAdapter.INSTANT.decode(bytes)
62+
assertThat(decoded.getEpochSecond()).isEqualTo(253402300799L)
63+
assertThat(decoded.getNano()).isEqualTo(999_999_999)
64+
}
65+
66+
@Test fun instantEncodeRejectsSecondsBelowMin() {
67+
// 0001-01-01T00:00:00Z - 1 second.
68+
val instant = ofEpochSecond(-62135596801L, 0L)
69+
assertFailure {
70+
ProtoAdapter.INSTANT.encode(instant)
71+
}.isInstanceOf<IllegalArgumentException>()
72+
}
73+
74+
@Test fun instantEncodeRejectsSecondsAboveMax() {
75+
// 9999-12-31T23:59:59Z + 1 second.
76+
val instant = ofEpochSecond(253402300800L, 0L)
77+
assertFailure {
78+
ProtoAdapter.INSTANT.encode(instant)
79+
}.isInstanceOf<IllegalArgumentException>()
80+
}
81+
82+
@Test fun instantEncodedSizeRejectsOutOfRange() {
83+
// 0001-01-01T00:00:00Z - 1 second.
84+
val instant = ofEpochSecond(-62135596801L, 0L)
85+
assertFailure {
86+
ProtoAdapter.INSTANT.encodedSize(instant)
87+
}.isInstanceOf<IllegalArgumentException>()
88+
}
4789
}

0 commit comments

Comments
 (0)