diff --git a/wire-protoc-compatibility-tests/src/test/java/com/squareup/wire/Proto3WireProtocCompatibilityTests.kt b/wire-protoc-compatibility-tests/src/test/java/com/squareup/wire/Proto3WireProtocCompatibilityTests.kt index a5f43f007a..e863fcede7 100644 --- a/wire-protoc-compatibility-tests/src/test/java/com/squareup/wire/Proto3WireProtocCompatibilityTests.kt +++ b/wire-protoc-compatibility-tests/src/test/java/com/squareup/wire/Proto3WireProtocCompatibilityTests.kt @@ -518,14 +518,14 @@ class Proto3WireProtocCompatibilityTests { val googleMessage = PizzaOuterClass.PizzaDelivery.newBuilder() .setOrderedAt( Timestamp.newBuilder() - .setSeconds(-631152000000L) // 1950-01-01T00:00:00.250Z. + .setSeconds(-631152000L) // 1950-01-01T00:00:00.250Z. .setNanos(250_000_000) .build(), ) .build() val wireMessage = PizzaDeliveryK( - ordered_at = ofEpochSecond(-631152000000L, 250_000_000L), + ordered_at = ofEpochSecond(-631152000L, 250_000_000L), ) val googleMessageBytes = googleMessage.toByteArray() diff --git a/wire-runtime/src/commonMain/kotlin/com/squareup/wire/Instant.kt b/wire-runtime/src/commonMain/kotlin/com/squareup/wire/Instant.kt index 8bdb1be5ed..b3cce0439d 100644 --- a/wire-runtime/src/commonMain/kotlin/com/squareup/wire/Instant.kt +++ b/wire-runtime/src/commonMain/kotlin/com/squareup/wire/Instant.kt @@ -27,6 +27,9 @@ expect class Instant { * * For example, this value will be -1 for the instant 1969-12-31T23:59:59Z, and 1 for the instant * 1970-01-01T00:00:01Z. + * + * It must be between -62135596800 and 253402300799 inclusive (which corresponds to + * 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z). */ fun getEpochSecond(): Long diff --git a/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt b/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt index 26553d44ba..38257194f5 100644 --- a/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt +++ b/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt @@ -1277,6 +1277,20 @@ internal fun commonDuration(): ProtoAdapter = object : ProtoAdapter = object : ProtoAdapter( LENGTH_DELIMITED, Instant::class, @@ -1284,25 +1298,28 @@ internal fun commonInstant(): ProtoAdapter = object : ProtoAdapter() } + + @Test fun instantEncodeValidMinBoundary() { + // 0001-01-01T00:00:00Z. + val instant = ofEpochSecond(-62135596800L, 0L) + val bytes = ProtoAdapter.INSTANT.encode(instant) + assertThat(ProtoAdapter.INSTANT.decode(bytes).getEpochSecond()).isEqualTo(-62135596800L) + } + + @Test fun instantEncodeValidMaxBoundary() { + // 9999-12-31T23:59:59Z. + val instant = ofEpochSecond(253402300799L, 999_999_999L) + val bytes = ProtoAdapter.INSTANT.encode(instant) + val decoded = ProtoAdapter.INSTANT.decode(bytes) + assertThat(decoded.getEpochSecond()).isEqualTo(253402300799L) + assertThat(decoded.getNano()).isEqualTo(999_999_999) + } + + @Test fun instantEncodeRejectsSecondsBelowMin() { + // 0001-01-01T00:00:00Z - 1 second. + val instant = ofEpochSecond(-62135596801L, 0L) + assertFailure { + ProtoAdapter.INSTANT.encode(instant) + }.isInstanceOf() + } + + @Test fun instantEncodeRejectsSecondsAboveMax() { + // 9999-12-31T23:59:59Z + 1 second. + val instant = ofEpochSecond(253402300800L, 0L) + assertFailure { + ProtoAdapter.INSTANT.encode(instant) + }.isInstanceOf() + } + + @Test fun instantEncodedSizeRejectsOutOfRange() { + // 0001-01-01T00:00:00Z - 1 second. + val instant = ofEpochSecond(-62135596801L, 0L) + assertFailure { + ProtoAdapter.INSTANT.encodedSize(instant) + }.isInstanceOf() + } }