Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gax): prevent truncation/overflow when converting time values #3095

Merged
merged 5 commits into from
Aug 29, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,27 @@ public static java.time.Duration toJavaTimeDuration(org.threeten.bp.Duration sou
if (source == null) {
return null;
}
return java.time.Duration.ofNanos(source.toNanos());
return java.time.Duration.ofSeconds(source.getSeconds(), source.getNano());
}

public static org.threeten.bp.Duration toThreetenDuration(java.time.Duration source) {
if (source == null) {
return null;
}
return org.threeten.bp.Duration.ofNanos(source.toNanos());
return org.threeten.bp.Duration.ofSeconds(source.getSeconds(), source.getNano());
}

public static java.time.Instant toJavaTimeInstant(org.threeten.bp.Instant source) {
if (source == null) {
return null;
}
return java.time.Instant.ofEpochMilli(source.toEpochMilli());
return java.time.Instant.ofEpochSecond(source.getEpochSecond(), source.getNano());
}

public static org.threeten.bp.Instant toThreetenInstant(java.time.Instant source) {
if (source == null) {
return null;
}
return org.threeten.bp.Instant.ofEpochMilli(source.toEpochMilli());
return org.threeten.bp.Instant.ofEpochSecond(source.getEpochSecond(), source.getNano());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@

public class TimeConversionUtilsTest {

// 0.999999999 seconds (1 second - 1 nano) - we need to subtract the nano or the Duration would
// overflow otherwise
final long MAX_DURATION_NANOS =
1 * 1000 /*=1 micro*/ * 1000 /*=1 milli*/ * 1000 /*=1 second*/ - 1;

// Arbitrary durations/instants to confirm conversion works as expected
final org.threeten.bp.Duration ttDuration = org.threeten.bp.Duration.ofMillis(123);
final org.threeten.bp.Instant ttInstant = org.threeten.bp.Instant.ofEpochMilli(123);
final java.time.Duration jtDuration = java.time.Duration.ofMillis(345);
Expand Down Expand Up @@ -69,4 +75,44 @@ void testToThreetenTimeInstant_validInput_succeeds() {
jtInstant.toEpochMilli(), TimeConversionUtils.toThreetenInstant(jtInstant).toEpochMilli());
assertNull(TimeConversionUtils.toThreetenInstant(null));
}

@Test
void testToThreeteenInstant_bigInput_doesNotOverflow() {
// defaults to MAX_SECONDS plus the max value of long for the nanos part
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too familiar with this. What do you mean by plus the max value of long for nanos?

I see that the MAX is 1000000000-12-31T23:59:59.999999999Z. Does this mean that this is essentially where jtInstant.getNano() and jtInstant.getEpochSecond() are both Interger.MAX_VALUE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instants and Durations have two components: the seconds part and the nanos part, both being long for Duration, and long for seconds + int for nanos in Instant.

Does this mean that this is essentially where jtInstant.getNano() and jtInstant.getEpochSecond() are both Interger.MAX_VALUE?

Yes, but with Long.MAX_VALUE for the seconds part instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok gotcha. I couldn't make sense of where the overflow was originally coming from since MAX_VALUE itself shouldn't trigger an overflow. If I'm understanding this correctly, it was triggered from the old way of how we were converting.

The toNanos() method was limited to Long.MAX_VALUE which has a limit of 292 years. Since Instant.MAX_VALUE has Long.MAX_VALUE for both seconds and nanos, it wouldn't be able to fit into toNanos() into a Long.MAX_VALUE as adding all the seconds + nanos would easily overfill.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding this correctly, it was triggered from the old way of how we were converting.

Exactly.

it wouldn't be able to fit into toNanos() into a Long.MAX_VALUE as adding all the seconds + nanos would easily overfill

Yes. I don't think we will ever deal with durations instants that last longer than a few hours but it's good to support the theoretical limits of these classes, even because a user may just try a max value and that may break the library.

java.time.Instant jtInstant = java.time.Instant.MAX;
org.threeten.bp.Instant ttInstant = TimeConversionUtils.toThreetenInstant(jtInstant);
assertEquals(jtInstant.getEpochSecond(), ttInstant.getEpochSecond());
assertEquals(jtInstant.getNano(), ttInstant.getNano());
}

@Test
void testToJavaTimeInstant_bigInput_doesNotOverflow() {
// defaults to MAX_SECONDS plus the max value of long for the nanos part
org.threeten.bp.Instant ttInstant = org.threeten.bp.Instant.MAX;
java.time.Instant jtInstant = TimeConversionUtils.toJavaTimeInstant(ttInstant);
assertEquals(jtInstant.getEpochSecond(), ttInstant.getEpochSecond());
assertEquals(jtInstant.getNano(), ttInstant.getNano());
}

@Test
void testToThreeteenDuration_bigInput_doesNotOverflow() {
// we use the max long value for the seconds part and an arbitrary int for the nanos part, so we
// can confirm that both components are preserved
java.time.Duration jtDuration =
java.time.Duration.ofSeconds(Long.MAX_VALUE, MAX_DURATION_NANOS);
org.threeten.bp.Duration ttDuration = TimeConversionUtils.toThreetenDuration(jtDuration);
assertEquals(jtDuration.getSeconds(), ttDuration.getSeconds());
assertEquals(jtDuration.getNano(), ttDuration.getNano());
}

@Test
void testToJavaTimeDuration_bigInput_doesNotOverflow() {
// we use the max long value for the seconds part and an arbitrary int for the nanos part, so we
// can confirm that both components are preserved
org.threeten.bp.Duration ttDuration =
org.threeten.bp.Duration.ofSeconds(Long.MAX_VALUE, MAX_DURATION_NANOS);
java.time.Duration jtDuration = TimeConversionUtils.toJavaTimeDuration(ttDuration);
assertEquals(jtDuration.getSeconds(), ttDuration.getSeconds());
assertEquals(jtDuration.getNano(), ttDuration.getNano());
}
}
Loading