Merge changes from topics "bug118835133_2", "bug118835133_1"

* changes:
  Fix android.text.format.Time behavior
  Add a test for behavior around Integer.MAX_VALUE
  Change behavior for times < first transition
diff --git a/luni/src/main/java/libcore/util/ZoneInfo.java b/luni/src/main/java/libcore/util/ZoneInfo.java
index 25cd402..65df611 100644
--- a/luni/src/main/java/libcore/util/ZoneInfo.java
+++ b/luni/src/main/java/libcore/util/ZoneInfo.java
@@ -86,7 +86,16 @@
     // Proclaim serialization compatibility with pre-OpenJDK AOSP
     static final long serialVersionUID = -4598738130123921552L;
 
+    /**
+     * The (best guess) non-DST offset used "today". It is stored in milliseconds.
+     * See also {@link #mOffsets} which holds values relative to this value, albeit in seconds.
+     */
     private int mRawOffset;
+
+    /**
+     * The earliest non-DST offset for the zone. It is stored in milliseconds and is absolute, i.e.
+     * it is not relative to mRawOffset.
+     */
     private final int mEarliestRawOffset;
 
     /**
@@ -283,15 +292,16 @@
         setID(name);
 
         // Find the latest daylight and standard offsets (if any).
-        int lastStd = -1;
-        int lastDst = -1;
-        for (int i = mTransitions.length - 1; (lastStd == -1 || lastDst == -1) && i >= 0; --i) {
-            int type = mTypes[i] & 0xff;
-            if (lastStd == -1 && mIsDsts[type] == 0) {
-                lastStd = i;
+        int lastStdTransitionIndex = -1;
+        int lastDstTransitionIndex = -1;
+        for (int i = mTransitions.length - 1;
+                (lastStdTransitionIndex == -1 || lastDstTransitionIndex == -1) && i >= 0; --i) {
+            int typeIndex = mTypes[i] & 0xff;
+            if (lastStdTransitionIndex == -1 && mIsDsts[typeIndex] == 0) {
+                lastStdTransitionIndex = i;
             }
-            if (lastDst == -1 && mIsDsts[type] != 0) {
-                lastDst = i;
+            if (lastDstTransitionIndex == -1 && mIsDsts[typeIndex] != 0) {
+                lastDstTransitionIndex = i;
             }
         }
 
@@ -300,19 +310,19 @@
             // If there are no transitions then use the first GMT offset.
             mRawOffset = gmtOffsets[0];
         } else {
-            if (lastStd == -1) {
+            if (lastStdTransitionIndex == -1) {
                 throw new IllegalStateException( "ZoneInfo requires at least one non-DST "
                         + "transition to be provided for each timezone that has at least one "
                         + "transition but could not find one for '" + name + "'");
             }
-            mRawOffset = gmtOffsets[mTypes[lastStd] & 0xff];
+            mRawOffset = gmtOffsets[mTypes[lastStdTransitionIndex] & 0xff];
         }
 
-        if (lastDst != -1) {
+        if (lastDstTransitionIndex != -1) {
             // Check to see if the last DST transition is in the future or the past. If it is in
             // the past then we treat it as if it doesn't exist, at least for the purposes of
             // setting mDstSavings and mUseDst.
-            long lastDSTTransitionTime = mTransitions[lastDst];
+            long lastDSTTransitionTime = mTransitions[lastDstTransitionIndex];
 
             // Convert the current time in millis into seconds. Unlike other places that convert
             // time in milliseconds into seconds in order to compare with transition time this
@@ -328,11 +338,11 @@
             // useDaylightTime at the start of 2009 but "false" at the end. This seems appropriate.
             if (lastDSTTransitionTime < currentUnixTimeSeconds) {
                 // The last DST transition is before now so treat it as if it doesn't exist.
-                lastDst = -1;
+                lastDstTransitionIndex = -1;
             }
         }
 
-        if (lastDst == -1) {
+        if (lastDstTransitionIndex == -1) {
             // There were no DST transitions or at least no future DST transitions so DST is not
             // used.
             mDstSavings = 0;
@@ -340,22 +350,31 @@
         } else {
             // Use the latest transition's pair of offsets to compute the DST savings.
             // This isn't generally useful, but it's exposed by TimeZone.getDSTSavings.
-            int lastGmtOffset = gmtOffsets[mTypes[lastStd] & 0xff];
-            int lastDstOffset = gmtOffsets[mTypes[lastDst] & 0xff];
+            int lastGmtOffset = gmtOffsets[mTypes[lastStdTransitionIndex] & 0xff];
+            int lastDstOffset = gmtOffsets[mTypes[lastDstTransitionIndex] & 0xff];
             mDstSavings = (lastDstOffset - lastGmtOffset) * 1000;
             mUseDst = true;
         }
 
-        // Cache the oldest known raw offset, in case we're asked about times that predate our
-        // transition data.
-        int firstStd = -1;
-        for (int i = 0; i < mTransitions.length; ++i) {
-            if (mIsDsts[mTypes[i] & 0xff] == 0) {
-                firstStd = i;
+        // From the tzfile docs (Jan 2019):
+        // The localtime(3) function uses the first standard-time ttinfo structure
+        // in the file (or simply the first ttinfo structure in the absence of a
+        // standard-time structure) if either tzh_timecnt is zero or the time
+        // argument is less than the first transition time recorded in the file.
+        //
+        // Cache the raw offset associated with the first nonDst type, in case we're asked about
+        // times that predate our transition data. Android falls back to mRawOffset if there are
+        // only DST ttinfo structures (assumed rare).
+        int firstStdTypeIndex = -1;
+        for (int i = 0; i < mIsDsts.length; ++i) {
+            if (mIsDsts[i] == 0) {
+                firstStdTypeIndex = i;
                 break;
             }
         }
-        int earliestRawOffset = (firstStd != -1) ? gmtOffsets[mTypes[firstStd] & 0xff] : mRawOffset;
+
+        int earliestRawOffset = (firstStdTypeIndex != -1)
+                ? gmtOffsets[firstStdTypeIndex] : mRawOffset;
 
         // Rather than keep offsets from UTC, we use offsets from local time, so the raw offset
         // can be changed and automatically affect all the offsets.
@@ -753,8 +772,9 @@
                     int offsetIndex = zoneInfo.findOffsetIndexForTimeInSeconds(timeSeconds);
                     if (offsetIndex == -1) {
                         // -1 means timeSeconds is "before the first recorded transition". The first
-                        // recorded transition is treated as a transition from non-DST and the raw
-                        // offset.
+                        // recorded transition is treated as a transition from non-DST and the
+                        // earliest known raw offset.
+                        offsetSeconds = zoneInfo.mEarliestRawOffset / 1000;
                         isDst = 0;
                     } else {
                         offsetSeconds += zoneInfo.mOffsets[offsetIndex];
@@ -763,7 +783,7 @@
                 }
 
                 // Perform arithmetic that might underflow before setting fields.
-                int wallTimeSeconds = checkedAdd(timeSeconds, offsetSeconds);
+                int wallTimeSeconds = checked32BitAdd(timeSeconds, offsetSeconds);
 
                 // Set fields.
                 calendar.setTimeInMillis(wallTimeSeconds * 1000L);
@@ -816,7 +836,7 @@
             try {
                 final int wallTimeSeconds =  (int) longWallTimeSeconds;
                 final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000;
-                final int rawTimeSeconds = checkedSubtract(wallTimeSeconds, rawOffsetSeconds);
+                final int rawTimeSeconds = checked32BitSubtract(wallTimeSeconds, rawOffsetSeconds);
 
                 if (zoneInfo.mTransitions.length == 0) {
                     // There is no transition information. There is just a raw offset for all time.
@@ -901,10 +921,11 @@
                 int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j];
                 int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds();
                 int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds;
-                int adjustedWallTimeSeconds = checkedAdd(oldWallTimeSeconds, adjustmentSeconds);
+                int adjustedWallTimeSeconds =
+                        checked32BitAdd(oldWallTimeSeconds, adjustmentSeconds);
                 if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) {
                     // Perform any arithmetic that might overflow.
-                    int returnValue = checkedSubtract(adjustedWallTimeSeconds,
+                    int returnValue = checked32BitSubtract(adjustedWallTimeSeconds,
                             targetIntervalOffsetSeconds);
 
                     // Modify field state and return the result.
@@ -1038,8 +1059,8 @@
                             // the result might be a DST or a non-DST answer for wall times that can
                             // exist in two OffsetIntervals.
                             int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds();
-                            int returnValue = checkedSubtract(wallTimeSeconds,
-                                    totalOffsetSeconds);
+                            int returnValue =
+                                    checked32BitSubtract(wallTimeSeconds, totalOffsetSeconds);
 
                             copyFieldsFromCalendar();
                             this.isDst = offsetInterval.getIsDst();
@@ -1226,16 +1247,19 @@
      * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when
      * PDT ended and PST began.
      *
-     * <p>For convenience all wall-time values are represented as the number of seconds since the
-     * beginning of the Unix epoch <em>in UTC</em>. To convert from a wall-time to the actual time
-     * in the offset it is necessary to <em>subtract</em> the {@code totalOffsetSeconds}.
+     * <p>Although wall-time means "local time", for convenience all wall-time values are stored in
+     * the number of seconds since the beginning of the Unix epoch to get that time <em>in UTC</em>.
+     * To convert from a wall-time to the actual UTC time it is necessary to <em>subtract</em> the
+     * {@code totalOffsetSeconds}.
      * For example: If the offset in PST is -07:00 hours, then:
      * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds
      * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST
      */
     static class OffsetInterval {
 
+        /** The time the interval starts in seconds since start of epoch, inclusive. */
         private final int startWallTimeSeconds;
+        /** The time the interval ends in seconds since start of epoch, exclusive. */
         private final int endWallTimeSeconds;
         private final int isDst;
         private final int totalOffsetSeconds;
@@ -1243,40 +1267,60 @@
         /**
          * Creates an {@link OffsetInterval}.
          *
-         * <p>If {@code transitionIndex} is -1, the transition is synthesized to be a non-DST offset
-         * that runs from the beginning of time until the first transition in {@code timeZone} and
-         * has an offset of {@code timezone.mRawOffset}. If {@code transitionIndex} is the last
-         * transition that transition is considered to run until the end of representable time.
+         * <p>If {@code transitionIndex} is -1, where possible the transition is synthesized to run
+         * from the beginning of 32-bit time until the first transition in {@code timeZone} with
+         * offset information based on the first type defined. If {@code transitionIndex} is the
+         * last transition, that transition is considered to run until the end of 32-bit time.
          * Otherwise, the information is extracted from {@code timeZone.mTransitions},
-         * {@code timeZone.mOffsets} an {@code timeZone.mIsDsts}.
+         * {@code timeZone.mOffsets} and {@code timeZone.mIsDsts}.
+         *
+         * <p>This method can return null when:
+         * <ol>
+         * <li>the {@code transitionIndex} is outside the allowed range, i.e.
+         *   {@code transitionIndex < -1 || transitionIndex >= [the number of transitions]}.</li>
+         * <li>when calculations result in a zero-length interval. This is only expected to occur
+         *   when dealing with transitions close to (or exactly at) {@code Integer.MIN_VALUE} and
+         *   {@code Integer.MAX_VALUE} and where it's difficult to convert from UTC to local times.
+         *   </li>
+         * </ol>
          */
-        public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex)
-                throws CheckedArithmeticException {
-
+        public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex) {
             if (transitionIndex < -1 || transitionIndex >= timeZone.mTransitions.length) {
                 return null;
             }
 
-            int rawOffsetSeconds = timeZone.mRawOffset / 1000;
             if (transitionIndex == -1) {
-                int endWallTimeSeconds = checkedAdd(timeZone.mTransitions[0], rawOffsetSeconds);
-                return new OffsetInterval(Integer.MIN_VALUE, endWallTimeSeconds, 0 /* isDst */,
-                        rawOffsetSeconds);
+                int totalOffsetSeconds = timeZone.mEarliestRawOffset / 1000;
+                int isDst = 0;
+
+                int startWallTimeSeconds = Integer.MIN_VALUE;
+                int endWallTimeSeconds =
+                        saturated32BitAdd(timeZone.mTransitions[0], totalOffsetSeconds);
+                if (startWallTimeSeconds == endWallTimeSeconds) {
+                    // There's no point in returning an OffsetInterval that lasts 0 seconds.
+                    return null;
+                }
+                return new OffsetInterval(startWallTimeSeconds, endWallTimeSeconds, isDst,
+                        totalOffsetSeconds);
             }
 
+            int rawOffsetSeconds = timeZone.mRawOffset / 1000;
             int type = timeZone.mTypes[transitionIndex] & 0xff;
             int totalOffsetSeconds = timeZone.mOffsets[type] + rawOffsetSeconds;
             int endWallTimeSeconds;
             if (transitionIndex == timeZone.mTransitions.length - 1) {
-                // If this is the last transition, make up the end time.
                 endWallTimeSeconds = Integer.MAX_VALUE;
             } else {
-                endWallTimeSeconds = checkedAdd(timeZone.mTransitions[transitionIndex + 1],
-                        totalOffsetSeconds);
+                endWallTimeSeconds = saturated32BitAdd(
+                        timeZone.mTransitions[transitionIndex + 1], totalOffsetSeconds);
             }
             int isDst = timeZone.mIsDsts[type];
             int startWallTimeSeconds =
-                    checkedAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds);
+                    saturated32BitAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds);
+            if (startWallTimeSeconds == endWallTimeSeconds) {
+                // There's no point in returning an OffsetInterval that lasts 0 seconds.
+                return null;
+            }
             return new OffsetInterval(
                     startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds);
         }
@@ -1317,11 +1361,11 @@
     }
 
     /**
-     * Calculate (a + b).
+     * Calculate (a + b). The result must be in the Integer range otherwise an exception is thrown.
      *
      * @throws CheckedArithmeticException if overflow or underflow occurs
      */
-    private static int checkedAdd(long a, int b) throws CheckedArithmeticException {
+    private static int checked32BitAdd(long a, int b) throws CheckedArithmeticException {
         // Adapted from Guava IntMath.checkedAdd();
         long result = a + b;
         if (result != (int) result) {
@@ -1331,16 +1375,30 @@
     }
 
     /**
-     * Calculate (a - b).
+     * Calculate (a - b). The result must be in the Integer range otherwise an exception is thrown.
      *
      * @throws CheckedArithmeticException if overflow or underflow occurs
      */
-    private static int checkedSubtract(int a, int b) throws CheckedArithmeticException {
+    private static int checked32BitSubtract(long a, int b) throws CheckedArithmeticException {
         // Adapted from Guava IntMath.checkedSubtract();
-        long result = (long) a - b;
+        long result = a - b;
         if (result != (int) result) {
             throw new CheckedArithmeticException();
         }
         return (int) result;
     }
+
+    /**
+     * Calculate (a + b). If the result would overflow or underflow outside of the Integer range
+     * Integer.MAX_VALUE or Integer.MIN_VALUE will be returned, respectively.
+     */
+    private static int saturated32BitAdd(long a, int b) {
+        long result = a + b;
+        if (result > Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        } else if (result < Integer.MIN_VALUE) {
+            return Integer.MIN_VALUE;
+        }
+        return (int) result;
+    }
 }
diff --git a/luni/src/test/java/libcore/java/util/TimeZoneTest.java b/luni/src/test/java/libcore/java/util/TimeZoneTest.java
index 6243359..68b6a54 100644
--- a/luni/src/test/java/libcore/java/util/TimeZoneTest.java
+++ b/luni/src/test/java/libcore/java/util/TimeZoneTest.java
@@ -82,17 +82,25 @@
     }
 
     public void testPreHistoricOffsets() throws Exception {
-        // "Africa/Bissau" has just a few transitions and hasn't changed in a long time.
-        // 1912-01-01 00:02:19-0100 ... 1912-01-01 00:02:20-0100
-        // 1974-12-31 23:59:59-0100 ... 1975-01-01 01:00:00+0000
+        // Note: This test changed after P to account for previously incorrect handling of
+        // prehistoric offsets. http://b/118835133
+        // "Africa/Bissau" has just a few transitions:
+        // Date, Offset, IsDst
+        // 1901-12-13 20:45:52,-3740,0 (Integer.MIN_VALUE, implicit with zic <= 2014b)
+        // 1912-01-01 01:00:00,-3600,0
+        // 1975-01-01 01:00:00,0,0
         TimeZone tz = TimeZone.getTimeZone("Africa/Bissau");
 
-        // Times before our first transition should assume we're still following that transition.
-        assertNonDaylightOffset(-3600, parseIsoTime("1911-01-01T00:00:00.0+0000"), tz);
+        // Before Integer.MIN_VALUE.
+        assertNonDaylightOffset(-3740, parseIsoTime("1900-01-01T00:00:00.0+0000"), tz);
 
+        // Times before 1912-01-01 01:00:00
+        assertNonDaylightOffset(-3740, parseIsoTime("1911-01-01T00:00:00.0+0000"), tz);
+
+        // Times after 1912-01-01 01:00:00 should use that transition.
         assertNonDaylightOffset(-3600, parseIsoTime("1912-01-01T12:00:00.0-0100"), tz);
 
-        // Times after our last transition should assume we're still following that transition.
+        // Times after 1975-01-01 01:00:00 should use that transition.
         assertNonDaylightOffset(0, parseIsoTime("1980-01-01T00:00:00.0+0000"), tz);
     }
 
@@ -309,18 +317,43 @@
         return String.format("GMT%c%02d:%02d", sign, offset / 60, offset % 60);
     }
 
-    // http://b/18839557
+    /**
+     * This test is to check for 32-bit integer overflow / underflow in TimeZone offset
+     * calculations. A bug (http://b/18839557) was reported when someone noticed that Android's
+     * TimeZone didn't produce the same answers as other libraries at times just outside the range
+     * of Integer seconds. The reason was because of int overflow / underflow which has been fixed.
+     * At the time of writing, Android's java.util.TimeZone implementation only supports reading
+     * TZif version 1 data (32-bit times) and provides one additional "before first transition"
+     * type. This makes Android's time zone information outside of the Integer range unreliable and
+     * unlikely to match libraries that use 64-bit times for transitions and/or calculate times
+     * outside of the range using rules (e.g. like ICU4J does).
+     */
     public void testOverflowing32BitUnixDates() {
         final TimeZone tz = TimeZone.getTimeZone("America/New_York");
 
+        // These times are significant because they are outside the 32-bit range for seconds.
+        final long beforeInt32Seconds = -2206292400L; // Thu, 01 Feb 1900 05:00:00 GMT
+        final long afterInt32Seconds = 2206292400L; // Wed, 30 Nov 2039 19:00:00 GMT
+
+        final long lowerTimeMillis = beforeInt32Seconds * 1000L;
+        final long upperTimeMillis = afterInt32Seconds * 1000L;
+
         // This timezone didn't have any daylight savings prior to 1917 and this
-        // date is sometime in 1901.
-        assertFalse(tz.inDaylightTime(new Date(-2206292400000L)));
-        assertEquals(-18000000, tz.getOffset(-2206292400000L));
+        // date is in 1900.
+        assertFalse(tz.inDaylightTime(new Date(lowerTimeMillis)));
+
+        // http://b/118835133:
+        // zic <= 2014b produces data that suggests before -1633280400 seconds (Sun, 31 Mar 1918
+        // 07:00:00 GMT) the offset was -18000000.
+        // zic > 2014b produces data that suggests before Integer.MIN_VALUE seconds the offset was
+        // -17762000 and between Integer.MIN_VALUE and -1633280400 it was -18000000. Once Android
+        // moves to zic > 2014b the -18000000 can be removed.
+        int actualOffset = tz.getOffset(lowerTimeMillis);
+        assertTrue(-18000000 == actualOffset || -17762000 == actualOffset);
 
         // Nov 30th 2039, no daylight savings as per current rules.
-        assertFalse(tz.inDaylightTime(new Date(2206292400000L)));
-        assertEquals(-18000000, tz.getOffset(2206292400000L));
+        assertFalse(tz.inDaylightTime(new Date(upperTimeMillis)));
+        assertEquals(-18000000, tz.getOffset(upperTimeMillis));
     }
 
     public void testTimeZoneIDLocalization() {
diff --git a/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java b/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java
index 91f1607..a151ee4 100644
--- a/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java
+++ b/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java
@@ -315,6 +315,134 @@
   }
 
   /**
+   * TimeZone APIs use long times in millis. Android uses TZif version 1 format data which
+   * uses 32-bit time values for transitions so it only gives accurate results for times in that
+   * range.
+   *
+   * <p>Newer versions of zic after 2014b introduce an explicit transition at the earliest
+   * representable time, which is Integer.MIN_VALUE for TZif version 1 files. Previously the type
+   * used was left implicit and readers were expected to use the first non-DST type in the file.
+   *
+   * <p>Testing newer zic versions demonstrated that Android had been mishandling the lookup of
+   * offset for times before the first transition. The logic has been corrected. This test would
+   * fail on versions of Android <= P.
+   */
+  public void testReadTimeZone_Bug118835133_extraFirstTransition() throws Exception {
+    // A time before the first representable time in a TZif version 1 file.
+    Instant before32BitTime = timeFromSeconds(Integer.MIN_VALUE).minusMillis(1);
+
+    // Times between the start of the 32-bit time range and the first "official" transition.
+    Instant[] earlyTimes = {
+            timeFromSeconds(Integer.MIN_VALUE),
+            timeFromSeconds(Integer.MIN_VALUE).plusMillis(1),
+    };
+
+    Instant firstRealTransitionTime = timeFromSeconds(1000);
+    Instant afterFirstRealTransitionTime = firstRealTransitionTime.plusSeconds(1);
+    Instant[] afterFirstRealTransitionTimes = {
+            firstRealTransitionTime,
+            afterFirstRealTransitionTime,
+    };
+
+    // A time to use as currentTime when building the TimeZone. Important for the getRawOffset()
+    // calculation.
+    Instant currentTime = afterFirstRealTransitionTime;
+
+    Duration type0Offset = offsetFromSeconds(0);
+    Duration type1Offset = offsetFromSeconds(1800);
+    Duration type2Offset = offsetFromSeconds(3600);
+    int[][] types = {
+            { offsetToSeconds(type0Offset), 0 }, // 1st type, used before first known transition.
+            { offsetToSeconds(type1Offset), 0 },
+            { offsetToSeconds(type2Offset), 0 },
+    };
+
+    // Creates a simulation of zic version <= 2014b where there is usually no explicit transition at
+    // Integer.MIN_VALUE seconds in TZif version 1 data.
+    {
+      int[][] transitions = {
+              { timeToSeconds(firstRealTransitionTime), 2 /* type 2 */ },
+      };
+      ZoneInfo oldZoneInfo = createZoneInfo(transitions, types, currentTime);
+      assertRawOffset(oldZoneInfo, type2Offset);
+
+      // We use the first non-DST type for times before the first transition.
+      assertOffsetAt(oldZoneInfo, type0Offset, before32BitTime);
+      assertOffsetAt(oldZoneInfo, type0Offset, earlyTimes);
+
+      // This is after the first transition, so type 2.
+      assertOffsetAt(oldZoneInfo, type2Offset, afterFirstRealTransitionTimes);
+    }
+
+    // Creates a simulation of zic version > 2014b where there is usually an explicit transition at
+    // Integer.MIN_VALUE seconds for TZif version 1 data.
+    {
+      int[][] transitions = {
+              { Integer.MIN_VALUE, 1 /* type 1 */ }, // The extra transition added by zic.
+              { timeToSeconds(firstRealTransitionTime), 2 /* type 2 */ },
+      };
+      ZoneInfo newZoneInfo = createZoneInfo(transitions, types, currentTime);
+      assertRawOffset(newZoneInfo, type2Offset);
+
+      // We use the first non-DST type for times before the first transition.
+      assertOffsetAt(newZoneInfo, type0Offset, before32BitTime);
+
+      // After the first transition, so type 1.
+      assertOffsetAt(newZoneInfo, type1Offset, earlyTimes);
+
+      // This is after the second transition, so type 2.
+      assertOffsetAt(newZoneInfo, type2Offset, afterFirstRealTransitionTimes);
+    }
+  }
+
+  /**
+   * Newer versions of zic after 2014b sometime introduce an explicit transition at
+   * Integer.MAX_VALUE.
+   */
+  public void testReadTimeZone_Bug118835133_extraLastTransition() throws Exception {
+    // An arbitrary time to use as currentTime. Not important for this test.
+    Instant currentTime = timeFromSeconds(4000);
+
+    // Offset before time 1000 should be consistent.
+    Instant[] timesToCheck = {
+            timeFromSeconds(2100), // arbitrary time > 2000
+            timeFromSeconds(Integer.MAX_VALUE).minusMillis(1),
+            timeFromSeconds(Integer.MAX_VALUE),
+            timeFromSeconds(Integer.MAX_VALUE).plusMillis(1),
+    };
+
+    int latestOffsetSeconds = 3600;
+    int[][] types = {
+            { 1800, 0 },
+            { latestOffsetSeconds, 0 },
+    };
+    Duration expectedLateOffset = offsetFromSeconds(latestOffsetSeconds);
+
+    // Create a simulation of zic version <= 2014b where there is usually no explicit transition at
+    // Integer.MAX_VALUE seconds.
+    {
+      int[][] transitions = {
+              { 1000, 0 },
+              { 2000, 1 },
+      };
+      ZoneInfo oldZoneInfo = createZoneInfo(transitions, types, currentTime);
+      assertOffsetAt(oldZoneInfo, expectedLateOffset, timesToCheck);
+    }
+
+    // Create a simulation of zic version > 2014b where there is sometimes an explicit transition at
+    // Integer.MAX_VALUE seconds.
+    {
+      int[][] transitions = {
+              { 1000, 0 },
+              { 2000, 1 },
+              { Integer.MAX_VALUE, 1}, // The extra transition.
+      };
+      ZoneInfo newZoneInfo = createZoneInfo(transitions, types, currentTime);
+      assertOffsetAt(newZoneInfo, expectedLateOffset, timesToCheck);
+    }
+  }
+
+  /**
    * Checks to make sure that it can handle up to 256 types.
    */
   public void testReadTimeZone_LotsOfTypes() throws Exception {
@@ -580,10 +708,26 @@
     return Instant.ofEpochSecond(timeInSeconds);
   }
 
+  private static int timeToSeconds(Instant time) {
+    long seconds = time.getEpochSecond();
+    if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) {
+      fail("Time out of seconds range: " + time);
+    }
+    return (int) seconds;
+  }
+
   private static Duration offsetFromSeconds(int offsetSeconds) {
     return Duration.ofSeconds(offsetSeconds);
   }
 
+  private static int offsetToSeconds(Duration offset) {
+    long seconds = offset.getSeconds();
+    if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) {
+      fail("Offset out of seconds range: " + offset);
+    }
+    return (int) seconds;
+  }
+
   private ZoneInfo createZoneInfo(int[][] transitions, int[][] types)
       throws Exception {
     return createZoneInfo(getName(), transitions, types, Instant.now());