Merge changes I9fc5738d,If4eee63b am: c206e20e48 am: dbc0aa539d am: 3a703d48fc am: 9a0b49d198

Change-Id: I0062441a24b75f74817e7ea4e002e4bbdc277afe
diff --git a/input_data/android/countryzones.txt b/input_data/android/countryzones.txt
index 25a0376..499c1f6 100644
--- a/input_data/android/countryzones.txt
+++ b/input_data/android/countryzones.txt
@@ -91,7 +91,15 @@
 # TimeZoneMapping:
 #
 # id:
-# The ID of the time zone.
+# The ID of the time zone to use on device. See also aliasId.
+#
+# aliasId:
+# (Optional) Used to identify the modern Olson ID when the id property is
+# using an obsoleted time zone ID. A legacy time zone ID may be used on device
+# to avoid problems if the zone ID is widely used. This is intentionally
+# explicit to make it clear the use of an old ID is intentional rather than an
+# accident. The id must also link to the aliasId in IANA's data (see the IANA
+# "backward" file).
 #
 # utcOffset:
 # The expected non-DST offset for the time zone. Used as a form of
diff --git a/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java b/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java
index 24c608c..e1e48f9 100644
--- a/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java
+++ b/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java
@@ -16,12 +16,17 @@
 
 package com.android.libcore.timezone.util;
 
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 
 /**
- * Stores context, errors and error severity for logging and flow control.
+ * Stores context, errors and error severity for logging and flow control. This class distinguishes
+ * between warnings (just info), errors (may not be immediately fatal) and fatal (immediately
+ * fatal).
  */
 public final class Errors {
 
@@ -45,19 +50,29 @@
         return scopes.removeLast();
     }
 
-    public void addFatal(String msg) {
-        level = Math.max(level, LEVEL_FATAL);
-        add(msg);
+    /** Adds a fatal error, and immediately throws {@link HaltExecutionException}. */
+    public HaltExecutionException addFatalAndHalt(String msg) throws HaltExecutionException {
+        addInternal(msg, null, LEVEL_FATAL);
+        throw new HaltExecutionException("Fatal error");
+    }
+
+    /** Adds a fatal error, and immediately throws {@link HaltExecutionException}. */
+    public HaltExecutionException addFatalAndHalt(String msg, Throwable t)
+            throws HaltExecutionException {
+        addInternal(msg, t, LEVEL_FATAL);
+        throw new HaltExecutionException("Fatal error");
     }
 
     public void addError(String msg) {
-        level = Math.max(level, LEVEL_ERROR);
-        add(msg);
+        addInternal(msg, null, LEVEL_ERROR);
+    }
+
+    public void addError(String msg, Throwable t) {
+        addInternal(msg, t, LEVEL_ERROR);
     }
 
     public void addWarning(String msg) {
-        level = Math.max(level, LEVEL_WARNING);
-        add(msg);
+        addInternal(msg, null, LEVEL_WARNING);
     }
 
     public String asString() {
@@ -73,15 +88,46 @@
         return messages.isEmpty();
     }
 
+    /** True if there are error or fatal messages. */
     public boolean hasError() {
         return level >= LEVEL_ERROR;
     }
 
+    /** True if there are fatal messages. */
     public boolean hasFatal() {
         return level >= LEVEL_FATAL;
     }
 
-    private void add(String msg) {
+    private void addInternal(String msg, Throwable t, int level) {
+        this.level = Math.max(this.level, level);
+        addMessage(msg);
+        if (t != null) {
+            try (StringWriter out = new StringWriter();
+                    PrintWriter printWriter = new PrintWriter(out)) {
+                t.printStackTrace(printWriter);
+                addMessage(out.toString());
+            } catch (IOException e) {
+                // Impossible - this is actually a compiler bug. Nothing throws IOException above.
+                throw new AssertionError("Impossible exception thrown", e);
+            }
+        }
+    }
+
+    private void addMessage(String msg) {
         messages.add(scopes.toString() + ": " + msg);
     }
+
+    /** Throws a {@link HaltExecutionException} if there are any error or fatal messages. */
+    public void throwIfError(String why) throws HaltExecutionException {
+        if (hasError()) {
+            throw new HaltExecutionException(why);
+        }
+    }
+
+    /** Thrown to halt execution. */
+    public static class HaltExecutionException extends Exception {
+        HaltExecutionException(String why) {
+            super(why);
+        }
+    }
 }
diff --git a/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java b/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java
index 5a6e717..1d568e5 100644
--- a/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java
+++ b/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java
@@ -20,8 +20,10 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.libcore.timezone.testing.TestUtils;
+import com.android.libcore.timezone.util.Errors.HaltExecutionException;
 
 public class ErrorsTest {
 
@@ -62,7 +64,10 @@
         assertFalse(errors.hasError());
         assertFalse(errors.hasFatal());
 
-        errors.addFatal("Hello");
+        try {
+            throw errors.addFatalAndHalt("Hello");
+        } catch (HaltExecutionException expected) {}
+
         assertFalse(errors.isEmpty());
         assertTrue(errors.hasError());
         assertTrue(errors.hasFatal());
@@ -80,13 +85,19 @@
         errors.addError("John Cleese");
 
         errors.pushScope("Holy grail");
-        errors.addFatal("Silly place");
+        try {
+            errors.addFatalAndHalt("Silly place");
+            fail();
+        } catch (HaltExecutionException expected) {}
         errors.popScope();
 
         errors.addError("Michael Palin");
 
         errors.pushScope("Parrot sketch");
-        errors.addFatal("Fjords");
+        try {
+            errors.addFatalAndHalt("Fjords");
+            fail();
+        } catch (HaltExecutionException expected) {}
         errors.popScope();
 
         String[] lines = errors.asString().split("\n");
diff --git a/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java b/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java
index 978d264..4626d8d 100644
--- a/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java
+++ b/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java
@@ -19,6 +19,7 @@
 
 import com.android.libcore.timezone.telephonylookup.proto.TelephonyLookupProtoFile;
 import com.android.libcore.timezone.util.Errors;
+import com.android.libcore.timezone.util.Errors.HaltExecutionException;
 
 import com.ibm.icu.util.ULocale;
 
@@ -68,78 +69,75 @@
     }
 
     boolean execute() throws IOException {
-        // Parse the countryzones input file.
-        TelephonyLookupProtoFile.TelephonyLookup telephonyLookupIn;
+        Errors errors = new Errors();
         try {
-            telephonyLookupIn = parseTelephonyLookupTextFile(telephonyLookupProtoFile);
-        } catch (ParseException e) {
-            logError("Unable to parse " + telephonyLookupProtoFile, e);
-            return false;
-        }
+            // Parse the countryzones input file.
+            TelephonyLookupProtoFile.TelephonyLookup telephonyLookupIn;
+            try {
+                telephonyLookupIn = parseTelephonyLookupTextFile(telephonyLookupProtoFile);
+            } catch (ParseException e) {
+                throw errors.addFatalAndHalt("Unable to parse " + telephonyLookupProtoFile, e);
+            }
 
-        List<TelephonyLookupProtoFile.Network> networksIn = telephonyLookupIn.getNetworksList();
+            List<TelephonyLookupProtoFile.Network> networksIn = telephonyLookupIn.getNetworksList();
 
-        Errors processingErrors = new Errors();
-        processingErrors.pushScope("Validation");
-        validateNetworks(networksIn, processingErrors);
-        processingErrors.popScope();
+            validateNetworks(networksIn, errors);
+            errors.throwIfError("One or more validation errors encountered");
 
-        // Validation failed, so stop.
-        if (processingErrors.hasFatal()) {
-            logInfo("Issues:\n" + processingErrors.asString());
-            return false;
-        }
-
-        TelephonyLookupXmlFile.TelephonyLookup telephonyLookupOut =
-                createOutputTelephonyLookup(networksIn);
-        if (!processingErrors.hasError()) {
-            // Write the output structure if there wasn't an error.
+            TelephonyLookupXmlFile.TelephonyLookup telephonyLookupOut =
+                    createOutputTelephonyLookup(networksIn);
             logInfo("Writing " + outputFile);
             try {
                 TelephonyLookupXmlFile.write(telephonyLookupOut, outputFile);
             } catch (XMLStreamException e) {
-                e.printStackTrace(System.err);
-                processingErrors.addFatal("Unable to write output file");
+                throw errors.addFatalAndHalt("Unable to write output file", e);
+            }
+        } catch (HaltExecutionException e) {
+            e.printStackTrace();
+            logError("Stopping due to fatal error: " + e.getMessage());
+        } finally {
+            // Report all warnings / errors
+            if (!errors.isEmpty()) {
+                logInfo("Issues:\n" + errors.asString());
             }
         }
-
-        // Report all warnings / errors
-        if (!processingErrors.isEmpty()) {
-            logInfo("Issues:\n" + processingErrors.asString());
-        }
-
-        return !processingErrors.hasError();
+        return !errors.hasError();
     }
 
-    private static void validateNetworks(
-            List<TelephonyLookupProtoFile.Network> networksIn, Errors processingErrors) {
-        Set<String> knownIsoCountries = getLowerCaseCountryIsoCodes();
-        Set<String> mccMncSet = new HashSet<>();
-        for (TelephonyLookupProtoFile.Network networkIn : networksIn) {
-            String mcc = networkIn.getMcc();
-            if (mcc.length() != 3 || !isAsciiNumeric(mcc)) {
-                processingErrors.addFatal("mcc=" + mcc + " must have 3 decimal digits");
-            }
+    private static void validateNetworks(List<TelephonyLookupProtoFile.Network> networksIn,
+            Errors errors) {
+        errors.pushScope("validateNetworks");
+        try {
+            Set<String> knownIsoCountries = getLowerCaseCountryIsoCodes();
+            Set<String> mccMncSet = new HashSet<>();
+            for (TelephonyLookupProtoFile.Network networkIn : networksIn) {
+                String mcc = networkIn.getMcc();
+                if (mcc.length() != 3 || !isAsciiNumeric(mcc)) {
+                    errors.addError("mcc=" + mcc + " must have 3 decimal digits");
+                }
 
-            String mnc = networkIn.getMnc();
-            if (!(mnc.length() == 2 || mnc.length() == 3) || !isAsciiNumeric(mnc)) {
-                processingErrors.addFatal("mnc=" + mnc + " must have 2 or 3 decimal digits");
-            }
+                String mnc = networkIn.getMnc();
+                if (!(mnc.length() == 2 || mnc.length() == 3) || !isAsciiNumeric(mnc)) {
+                    errors.addError("mnc=" + mnc + " must have 2 or 3 decimal digits");
+                }
 
-            String mccMnc = "" + mcc + mnc;
-            if (!mccMncSet.add(mccMnc)) {
-                processingErrors.addFatal("Duplicate entry for mcc=" + mcc + ", mnc=" + mnc);
-            }
+                String mccMnc = "" + mcc + mnc;
+                if (!mccMncSet.add(mccMnc)) {
+                    errors.addError("Duplicate entry for mcc=" + mcc + ", mnc=" + mnc);
+                }
 
-            String countryIsoCode = networkIn.getCountryIsoCode();
-            String countryIsoCodeLower = countryIsoCode.toLowerCase(Locale.ROOT);
-            if (!countryIsoCodeLower.equals(countryIsoCode)) {
-                processingErrors.addFatal("Country code not lower case: " + countryIsoCode);
-            }
+                String countryIsoCode = networkIn.getCountryIsoCode();
+                String countryIsoCodeLower = countryIsoCode.toLowerCase(Locale.ROOT);
+                if (!countryIsoCodeLower.equals(countryIsoCode)) {
+                    errors.addError("Country code not lower case: " + countryIsoCode);
+                }
 
-            if (!knownIsoCountries.contains(countryIsoCodeLower)) {
-                processingErrors.addFatal("Country code not known: " + countryIsoCode);
+                if (!knownIsoCountries.contains(countryIsoCodeLower)) {
+                    errors.addError("Country code not known: " + countryIsoCode);
+                }
             }
+        } finally {
+            errors.popScope();
         }
     }
 
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java
new file mode 100644
index 0000000..161a6b8
--- /dev/null
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.libcore.timezone.tzlookup;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A class that knows about the structure of the backward file.
+ */
+final class BackwardFile {
+
+    private final Map<String, String> links = new HashMap<>();
+
+    private BackwardFile() {}
+
+    static BackwardFile parse(String backwardFile) throws IOException, ParseException {
+        BackwardFile backward = new BackwardFile();
+
+        List<String> lines = Files
+                .readAllLines(Paths.get(backwardFile), StandardCharsets.US_ASCII);
+
+        // Remove comments
+        List<String> linkLines =
+                lines.stream()
+                        .filter(s -> !(s.startsWith("#") || s.isEmpty()))
+                        .collect(Collectors.toList());
+
+        for (String linkLine : linkLines) {
+            String[] fields = linkLine.split("\t+");
+            if (fields.length < 3 || !fields[0].equals("Link")) {
+                throw new ParseException("Line is malformed: " + linkLine, 0);
+            }
+            backward.addLink(fields[1], fields[2]);
+        }
+        return backward;
+    }
+
+    /**
+     * Add a link entry.
+     *
+     * @param target the new tz ID
+     * @param linkName the old tz ID
+     */
+    private void addLink(String target, String linkName) {
+        String oldValue = links.put(linkName, target);
+        if (oldValue != null) {
+            throw new IllegalStateException("Duplicate link from " + linkName);
+        }
+    }
+
+    /** Returns a mapping from linkName (old tz ID) to target (new tz ID). */
+    Map<String, String> getDirectLinks() {
+        // Validate links for cycles and collapse the links if there are links to links. There's a
+        // simple check to confirm that no chain is longer than a fixed length, to guard against
+        // cycles.
+        final int maxChainLength = 2;
+        Map<String, String> collapsedLinks = new HashMap<>();
+        for (String fromId : links.keySet()) {
+            int chainLength = 0;
+            String currentId = fromId;
+            String lastId = null;
+            while ((currentId = links.get(currentId)) != null) {
+                chainLength++;
+                lastId = currentId;
+                if (chainLength >= maxChainLength) {
+                    throw new IllegalStateException(
+                            "Chain from " + fromId + " is longer than " + maxChainLength);
+                }
+            }
+            if (chainLength == 0) {
+                throw new IllegalStateException("Null Link targetId for " + fromId);
+            }
+            collapsedLinks.put(fromId, lastId);
+        }
+        return Collections.unmodifiableMap(collapsedLinks);
+    }
+}
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
index 95525bf..b6d368b 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
@@ -19,6 +19,7 @@
 import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneTree;
 import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneUsage;
 import com.android.libcore.timezone.util.Errors;
+import com.android.libcore.timezone.util.Errors.HaltExecutionException;
 import com.ibm.icu.util.BasicTimeZone;
 import com.ibm.icu.util.Calendar;
 import com.ibm.icu.util.GregorianCalendar;
@@ -29,6 +30,7 @@
 import java.text.ParseException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -70,112 +72,171 @@
 
     private final String countryZonesFile;
     private final String zoneTabFile;
+    private final String backwardFile;
     private final String outputFile;
 
     /**
      * Executes the generator.
-     *
-     * Positional arguments:
-     * 1: The countryzones.txt file
-     * 2: the zone.tab file
-     * 3: the file to generate
      */
     public static void main(String[] args) throws Exception {
-        if (args.length != 3) {
+        if (args.length != 4) {
             System.err.println(
                     "usage: java com.android.libcore.timezone.tzlookup.TzLookupGenerator"
-                            + " <input proto file> <zone.tab file> <output xml file>");
+                            + " <input proto file> <zone.tab file> <backward file>"
+                            + " <output xml file>");
             System.exit(0);
         }
-        boolean success = new TzLookupGenerator(args[0], args[1], args[2]).execute();
+        boolean success = new TzLookupGenerator(args[0], args[1], args[2], args[3]).execute();
         System.exit(success ? 0 : 1);
     }
 
-    TzLookupGenerator(String countryZonesFile, String zoneTabFile, String outputFile) {
+    TzLookupGenerator(String countryZonesFile, String zoneTabFile, String backwardFile,
+            String outputFile) {
         this.countryZonesFile = countryZonesFile;
         this.zoneTabFile = zoneTabFile;
+        this.backwardFile = backwardFile;
         this.outputFile = outputFile;
     }
 
-    boolean execute() throws IOException {
-        // Parse the countryzones input file.
-        CountryZonesFile.CountryZones countryZonesIn;
+    boolean execute() {
+        Errors errors = new Errors();
         try {
-            countryZonesIn = CountryZonesFileSupport.parseCountryZonesTextFile(countryZonesFile);
-        } catch (ParseException e) {
-            logError("Unable to parse " + countryZonesFile, e);
-            return false;
-        }
+            // Parse the countryzones input file.
+            CountryZonesFile.CountryZones countryZonesIn =
+                    parseAndValidateCountryZones(countryZonesFile, errors);
 
-        // Check the countryzones rules version matches the version that ICU is using.
-        String icuTzDataVersion = TimeZone.getTZDataVersion();
-        String inputIanaVersion = countryZonesIn.getIanaVersion();
-        if (!icuTzDataVersion.equals(inputIanaVersion)) {
-            logError("Input data (countryzones.txt) is for " + inputIanaVersion
-                    + " but the ICU you have is for " + icuTzDataVersion);
-            return false;
-        }
+            // Check the countryzones.txt rules version matches the version that ICU is using.
+            String icuTzDataVersion = TimeZone.getTZDataVersion();
+            String inputIanaVersion = countryZonesIn.getIanaVersion();
+            if (!icuTzDataVersion.equals(inputIanaVersion)) {
+                throw errors.addFatalAndHalt("Input data (countryzones.txt) is for "
+                        + inputIanaVersion + " but the ICU you have is for " + icuTzDataVersion);
+            }
 
-        // Pull out information we want to validate against from zone.tab (which we have to assume
-        // matches the ICU version since it doesn't contain its own version info).
-        ZoneTabFile zoneTabIn = ZoneTabFile.parse(zoneTabFile);
-        Map<String, List<String>> zoneTabMapping =
-                ZoneTabFile.createCountryToOlsonIdsMap(zoneTabIn);
-        List<CountryZonesFile.Country> countriesIn = countryZonesIn.getCountriesList();
-        List<String> countriesInIsos = CountryZonesFileSupport.extractIsoCodes(countriesIn);
 
-        // Sanity check the countryzones file only contains lower-case country codes. The output
-        // file uses them and the on-device code assumes lower case.
-        if (!Utils.allLowerCaseAscii(countriesInIsos)) {
-            logError("Non-lowercase country ISO codes found in: " + countriesInIsos);
-            return false;
-        }
-        // Sanity check the countryzones file doesn't contain duplicate country entries.
-        if (!Utils.allUnique(countriesInIsos)) {
-            logError("Duplicate input country entries found: " + countriesInIsos);
-            return false;
-        }
+            // Pull out information we want to validate against from zone.tab (which we have to
+            // assume matches the ICU version since it doesn't contain its own version info).
+            Map<String, List<String>> zoneTabMapping = parseZoneTabFile(zoneTabFile, errors);
 
-        // Validate the country iso codes found in the countryzones against those in zone.tab.
-        // zone.tab uses upper case, countryzones uses lower case.
-        List<String> upperCaseCountriesInIsos = Utils.toUpperCase(countriesInIsos);
-        Set<String> timezonesCountryIsos = new HashSet<>(upperCaseCountriesInIsos);
-        Set<String> zoneTabCountryIsos = zoneTabMapping.keySet();
-        if (!zoneTabCountryIsos.equals(timezonesCountryIsos)) {
-            logError(zoneTabFile + " contains "
-                    + Utils.subtract(zoneTabCountryIsos, timezonesCountryIsos)
-                    + " not present in countryzones, "
-                    + countryZonesFile + " contains "
-                    + Utils.subtract(timezonesCountryIsos, zoneTabCountryIsos)
-                    + " not present in zonetab.");
-            return false;
-        }
+            List<CountryZonesFile.Country> countriesIn = countryZonesIn.getCountriesList();
+            List<String> countriesInIsos = CountryZonesFileSupport.extractIsoCodes(countriesIn);
 
-        Errors processingErrors = new Errors();
-        TzLookupFile.TimeZones timeZonesOut = createOutputTimeZones(
-                inputIanaVersion, zoneTabMapping, countriesIn, processingErrors);
-        if (!processingErrors.hasError()) {
+            // Sanity check the countryzones file only contains lower-case country codes. The output
+            // file uses them and the on-device code assumes lower case.
+            if (!Utils.allLowerCaseAscii(countriesInIsos)) {
+                throw errors.addFatalAndHalt(
+                        "Non-lowercase country ISO codes found in: " + countriesInIsos);
+            }
+            // Sanity check the countryzones file doesn't contain duplicate country entries.
+            if (!Utils.allUnique(countriesInIsos)) {
+                throw errors.addFatalAndHalt(
+                        "Duplicate input country entries found: " + countriesInIsos);
+            }
+
+            // Validate the country iso codes found in the countryzones.txt against those in
+            // zone.tab. zone.tab uses upper case, countryzones uses lower case.
+            List<String> upperCaseCountriesInIsos = Utils.toUpperCase(countriesInIsos);
+            Set<String> timezonesCountryIsos = new HashSet<>(upperCaseCountriesInIsos);
+            Set<String> zoneTabCountryIsos = zoneTabMapping.keySet();
+            if (!zoneTabCountryIsos.equals(timezonesCountryIsos)) {
+                throw errors.addFatalAndHalt(zoneTabFile + " contains "
+                        + Utils.subtract(zoneTabCountryIsos, timezonesCountryIsos)
+                        + " not present in countryzones, "
+                        + countryZonesFile + " contains "
+                        + Utils.subtract(timezonesCountryIsos, zoneTabCountryIsos)
+                        + " not present in zonetab.");
+            }
+
+            // Obtain and validate a mapping from old IDs to new IDs.
+            Map<String, String> zoneIdLinks = parseAndValidateBackwardFile(backwardFile, errors);
+            errors.throwIfError("Errors accumulated");
+
+            TzLookupFile.TimeZones timeZonesOut = createOutputTimeZones(
+                    inputIanaVersion, zoneTabMapping, countriesIn, zoneIdLinks, errors);
+            errors.throwIfError("Errors accumulated");
+
             // Write the output structure if there wasn't an error.
             logInfo("Writing " + outputFile);
             try {
                 TzLookupFile.write(timeZonesOut, outputFile);
             } catch (XMLStreamException e) {
-                e.printStackTrace(System.err);
-                processingErrors.addFatal("Unable to write output file");
+                throw errors.addFatalAndHalt("Unable to write output file", e);
+            }
+            return true;
+        } catch (HaltExecutionException | IOException e) {
+            logError("Stopping due to fatal condition", e);
+            return false;
+        } finally {
+            // Report all warnings / errors
+            if (!errors.isEmpty()) {
+                logInfo("Issues:\n" + errors.asString());
             }
         }
+    }
 
-        // Report all warnings / errors
-        if (!processingErrors.isEmpty()) {
-            logInfo("Issues:\n" + processingErrors.asString());
+    private Map<String, List<String>> parseZoneTabFile(String zoneTabFile, Errors errors)
+            throws HaltExecutionException {
+        errors.pushScope("Parsing " + zoneTabFile);
+        try {
+            ZoneTabFile zoneTabIn;
+            zoneTabIn = ZoneTabFile.parse(zoneTabFile);
+            return ZoneTabFile.createCountryToOlsonIdsMap(zoneTabIn);
+        } catch (ParseException | IOException e) {
+            throw errors.addFatalAndHalt("Unable to parse " + zoneTabFile, e);
+        } finally {
+            errors.popScope();
         }
+    }
 
-        return !processingErrors.hasError();
+    /**
+     * Load the backward file and return the links contained within. This is used as the source of
+     * equivalent time zone IDs.
+     */
+    private static Map<String, String> parseAndValidateBackwardFile(
+            String backwardFile, Errors errors) {
+        errors.pushScope("Parsing " + backwardFile);
+        try {
+            BackwardFile backwardIn = BackwardFile.parse(backwardFile);
+
+            // Validate the links.
+            Map<String, String> zoneIdLinks = backwardIn.getDirectLinks();
+            zoneIdLinks.forEach(
+                    (k, v) -> {
+                        if (invalidTimeZoneId(k)) {
+                            errors.addError("Bad 'from' link: " + k + "->" + v);
+                        }
+                        if (invalidTimeZoneId(v)) {
+                            errors.addError("Bad 'to' link: " + k + "->" + v);
+                        }
+                    });
+            return zoneIdLinks;
+        } catch (ParseException | IOException e) {
+            errors.addError("Unable to parse " + backwardFile, e);
+            return null;
+        } finally {
+            errors.popScope();
+        }
+    }
+
+    private static CountryZonesFile.CountryZones parseAndValidateCountryZones(
+            String countryZonesFile, Errors errors) throws HaltExecutionException {
+        errors.pushScope("Parsing " + countryZonesFile);
+        try {
+            CountryZonesFile.CountryZones countryZonesIn;
+            countryZonesIn = CountryZonesFileSupport.parseCountryZonesTextFile(countryZonesFile);
+            return countryZonesIn;
+        } catch (ParseException | IOException e) {
+            throw errors.addFatalAndHalt("Unable to parse " + countryZonesFile, e);
+        } finally {
+            errors.popScope();
+        }
     }
 
     private static TzLookupFile.TimeZones createOutputTimeZones(String inputIanaVersion,
             Map<String, List<String>> zoneTabMapping, List<CountryZonesFile.Country> countriesIn,
-            Errors processingErrors) {
+            Map<String, String> zoneIdLinks, Errors errors)
+            throws HaltExecutionException {
+
         // Start constructing the output structure.
         TzLookupFile.TimeZones timeZonesOut = new TzLookupFile.TimeZones(inputIanaVersion);
         TzLookupFile.CountryZones countryZonesOut = new TzLookupFile.CountryZones();
@@ -194,63 +255,61 @@
             String isoCode = countryIn.getIsoCode();
             List<String> zoneTabCountryTimeZoneIds = zoneTabMapping.get(isoCode.toUpperCase());
             if (zoneTabCountryTimeZoneIds == null) {
-                processingErrors.addError("Country=" + isoCode + " missing from zone.tab");
+                errors.addError("Country=" + isoCode + " missing from zone.tab");
                 // No point in continuing.
                 continue;
             }
 
             TzLookupFile.Country countryOut = processCountry(
                     offsetSampleTimeMillis, everUseUtcStartTimeMillis, countryIn,
-                    zoneTabCountryTimeZoneIds, processingErrors);
-            if (processingErrors.hasFatal()) {
-                // Stop if there's a fatal error, continue processing countries if there are just
-                // errors.
-                break;
-            } else if (countryOut == null) {
+                    zoneTabCountryTimeZoneIds, zoneIdLinks, errors);
+            if (countryOut == null) {
+                // Continue processing countries if there are only errors.
                 continue;
             }
             countryZonesOut.addCountry(countryOut);
         }
+        errors.throwIfError("One or more countries failed");
         return timeZonesOut;
     }
 
     private static TzLookupFile.Country processCountry(long offsetSampleTimeMillis,
             long everUseUtcStartTimeMillis, CountryZonesFile.Country countryIn,
-            List<String> zoneTabCountryTimeZoneIds,
-            Errors processingErrors) {
+            List<String> zoneTabCountryTimeZoneIds, Map<String, String> zoneIdLinks,
+            Errors errors) {
         String isoCode = countryIn.getIsoCode();
-        processingErrors.pushScope("country=" + isoCode);
+        errors.pushScope("country=" + isoCode);
         try {
             // Each Country must have >= 1 time zone.
             List<CountryZonesFile.TimeZoneMapping> timeZonesIn =
                     countryIn.getTimeZoneMappingsList();
             if (timeZonesIn.isEmpty()) {
-                processingErrors.addError("No time zones");
+                errors.addError("No time zones");
                 // No point in continuing.
                 return null;
             }
 
-            // Look for duplicate time zone IDs.
             List<String> countryTimeZoneIds = CountryZonesFileSupport.extractIds(timeZonesIn);
+
+            // Look for duplicate time zone IDs.
             if (!Utils.allUnique(countryTimeZoneIds)) {
-                processingErrors.addError("country's zones=" + countryTimeZoneIds
-                        + " contains duplicates");
+                errors.addError("country's zones=" + countryTimeZoneIds + " contains duplicates");
                 // No point in continuing.
                 return null;
             }
 
             // Each Country needs a default time zone ID (but we can guess in some cases).
-            String defaultTimeZoneId = determineCountryDefaultZoneId(countryIn, processingErrors);
+            String defaultTimeZoneId = determineCountryDefaultZoneId(countryIn, errors);
             if (defaultTimeZoneId == null) {
                 // No point in continuing.
                 return null;
             }
             boolean defaultTimeZoneBoost =
-                    determineCountryDefaultTimeZoneBoost(countryIn, processingErrors);
+                    determineCountryDefaultTimeZoneBoost(countryIn, errors);
 
             // Validate the default.
             if (!countryTimeZoneIds.contains(defaultTimeZoneId)) {
-                processingErrors.addError("defaultTimeZoneId=" + defaultTimeZoneId
+                errors.addError("defaultTimeZoneId=" + defaultTimeZoneId
                         + " is not one of the country's zones=" + countryTimeZoneIds);
                 // No point in continuing.
                 return null;
@@ -258,52 +317,49 @@
 
             // Validate the other zone IDs.
             try {
-                processingErrors.pushScope("validate country zone ids");
-                boolean errors = false;
+                errors.pushScope("validate country zone ids");
                 for (String countryTimeZoneId : countryTimeZoneIds) {
                     if (invalidTimeZoneId(countryTimeZoneId)) {
-                        processingErrors.addError("countryTimeZoneId=" + countryTimeZoneId
+                        errors.addError("countryTimeZoneId=" + countryTimeZoneId
                                 + " is not a valid zone ID");
-                        errors = true;
                     }
                 }
-                if (errors) {
+                if (errors.hasError()) {
                     // No point in continuing.
                     return null;
                 }
             } finally {
-                processingErrors.popScope();
+                errors.popScope();
             }
 
             // Work out the hint for whether the country uses a zero offset from UTC.
             boolean everUsesUtc = anyZonesUseUtc(countryTimeZoneIds, everUseUtcStartTimeMillis);
 
             // Validate the country information against the equivalent information in zone.tab.
-            processingErrors.pushScope("zone.tab comparison");
+            errors.pushScope("zone.tab comparison");
             try {
                 // Look for unexpected duplicate time zone IDs in zone.tab
                 if (!Utils.allUnique(zoneTabCountryTimeZoneIds)) {
-                    processingErrors.addError(
-                            "Duplicate time zone IDs found:" + zoneTabCountryTimeZoneIds);
+                    errors.addError("Duplicate time zone IDs found:" + zoneTabCountryTimeZoneIds);
                     // No point in continuing.
                     return null;
-
                 }
 
-                if (!Utils.setEquals(zoneTabCountryTimeZoneIds, countryTimeZoneIds)) {
-                    processingErrors.addError("IANA lists " + isoCode
-                            + " as having zones: " + zoneTabCountryTimeZoneIds
-                            + ", but countryzones has " + countryTimeZoneIds);
+                // Validate the IDs being used against the IANA data for the country. If it fails
+                // the countryzones.txt needs to be updated with new IDs (or an alias can be added
+                // if there's some reason to keep using the old ID).
+                validateCountryZonesTzIdsAgainstIana(isoCode, zoneTabCountryTimeZoneIds,
+                        timeZonesIn, zoneIdLinks, errors);
+                if (errors.hasError()) {
                     // No point in continuing.
                     return null;
                 }
             } finally {
-                processingErrors.popScope();
+                errors.popScope();
             }
 
             // Calculate countryZoneUsage.
-            CountryZoneUsage countryZoneUsage =
-                    calculateCountryZoneUsage(countryIn, processingErrors);
+            CountryZoneUsage countryZoneUsage = calculateCountryZoneUsage(countryIn, errors);
             if (countryZoneUsage == null) {
                 // No point in continuing with this country.
                 return null;
@@ -315,20 +371,18 @@
 
             // Process each input time zone.
             for (CountryZonesFile.TimeZoneMapping timeZoneIn : timeZonesIn) {
-                processingErrors.pushScope(
+                errors.pushScope(
                         "id=" + timeZoneIn.getId() + ", offset=" + timeZoneIn.getUtcOffset()
                                 + ", shownInPicker=" + timeZoneIn.getShownInPicker());
                 try {
                     // Validate the offset information in countryIn.
-                    validateNonDstOffset(offsetSampleTimeMillis, countryIn, timeZoneIn,
-                            processingErrors);
+                    validateNonDstOffset(offsetSampleTimeMillis, countryIn, timeZoneIn, errors);
 
                     String timeZoneInId = timeZoneIn.getId();
                     boolean shownInPicker = timeZoneIn.getShownInPicker();
                     if (!countryZoneUsage.hasEntry(timeZoneInId)) {
                         // This implies a programming error.
-                        processingErrors.addFatal(
-                                "No entry in CountryZoneUsage for " + timeZoneInId);
+                        errors.addError("No entry in CountryZoneUsage for " + timeZoneInId);
                         return null;
                     }
 
@@ -343,13 +397,44 @@
                                     timeZoneInId, shownInPicker, notUsedAfterInstant);
                     countryOut.addTimeZoneIdentifier(timeZoneIdOut);
                 } finally {
-                    processingErrors.popScope();
+                    errors.popScope();
                 }
             }
             return countryOut;
         } finally{
             // End of country processing.
-            processingErrors.popScope();
+            errors.popScope();
+        }
+    }
+
+    private static void validateCountryZonesTzIdsAgainstIana(String isoCode,
+            List<String> zoneTabCountryTimeZoneIds,
+            List<CountryZonesFile.TimeZoneMapping> timeZoneMappings,
+            Map<String, String> zoneIdLinks, Errors errors) {
+
+        List<String> expectedIanaTimeZoneIds = new ArrayList<>();
+        for (CountryZonesFile.TimeZoneMapping mapping : timeZoneMappings) {
+            String timeZoneId = mapping.getId();
+            String expectedIanaTimeZoneId;
+            if (!mapping.hasAliasId()) {
+                expectedIanaTimeZoneId = timeZoneId;
+            } else {
+                String aliasTimeZoneId = mapping.getAliasId();
+
+                // Confirm the alias is valid.
+                if (!aliasTimeZoneId.equals(zoneIdLinks.get(timeZoneId))) {
+                    errors.addError(timeZoneId + " does not link to " + aliasTimeZoneId);
+                    return;
+                }
+                expectedIanaTimeZoneId = aliasTimeZoneId;
+            }
+            expectedIanaTimeZoneIds.add(expectedIanaTimeZoneId);
+        }
+
+        if (!Utils.setEquals(zoneTabCountryTimeZoneIds, expectedIanaTimeZoneIds)) {
+            errors.addError("IANA lists " + isoCode
+                    + " as having zones: " + zoneTabCountryTimeZoneIds
+                    + ", but countryzones has " + expectedIanaTimeZoneIds);
         }
     }
 
@@ -357,20 +442,20 @@
      * Determines the default zone ID for the country.
      */
     private static String determineCountryDefaultZoneId(
-            CountryZonesFile.Country countryIn, Errors processingErrorsOut) {
+            CountryZonesFile.Country countryIn, Errors errors) {
         List<CountryZonesFile.TimeZoneMapping> timeZonesIn = countryIn.getTimeZoneMappingsList();
         String defaultTimeZoneId;
         if (countryIn.hasDefaultTimeZoneId()) {
             defaultTimeZoneId = countryIn.getDefaultTimeZoneId();
             if (invalidTimeZoneId(defaultTimeZoneId)) {
-                processingErrorsOut.addError(
+                errors.addError(
                         "Default time zone ID " + defaultTimeZoneId + " is not valid");
                 // No point in continuing.
                 return null;
             }
         } else {
             if (timeZonesIn.size() > 1) {
-                processingErrorsOut.addError(
+                errors.addError(
                         "To pick a default time zone there must be a single offset group");
                 // No point in continuing.
                 return null;
@@ -384,14 +469,14 @@
      * Determines the defaultTimeZoneBoost value for the country.
      */
     private static boolean determineCountryDefaultTimeZoneBoost(
-            CountryZonesFile.Country countryIn, Errors processingErrorsOut) {
+            CountryZonesFile.Country countryIn, Errors errors) {
         if (!countryIn.hasDefaultTimeZoneBoost()) {
             return false;
         }
 
         boolean defaultTimeZoneBoost = countryIn.getDefaultTimeZoneBoost();
         if (!countryIn.hasDefaultTimeZoneId() && defaultTimeZoneBoost) {
-            processingErrorsOut.addError(
+            errors.addError(
                     "defaultTimeZoneBoost is specified but defaultTimeZoneId is not explicit");
         }
 
@@ -459,7 +544,7 @@
         try {
             utcOffsetMillis = Utils.parseUtcOffsetToMillis(utcOffsetString);
         } catch (ParseException e) {
-            errors.addFatal("Bad offset string: " + utcOffsetString);
+            errors.addError("Bad offset string: " + utcOffsetString);
             return;
         }
 
@@ -471,7 +556,7 @@
 
         String timeZoneIdIn = timeZoneIn.getId();
         if (invalidTimeZoneId(timeZoneIdIn)) {
-            errors.addFatal("Time zone ID=" + timeZoneIdIn + " is not valid");
+            errors.addError("Time zone ID=" + timeZoneIdIn + " is not valid");
             return;
         }
 
@@ -481,7 +566,7 @@
         timeZone.getOffset(offsetSampleTimeMillis, false /* local */, offsets);
         int actualOffsetMillis = offsets[0];
         if (actualOffsetMillis != utcOffsetMillis) {
-            errors.addFatal("Offset mismatch: You will want to confirm the ordering for "
+            errors.addError("Offset mismatch: You will want to confirm the ordering for "
                     + country.getIsoCode() + " still makes sense. Raw offset for "
                     + timeZoneIdIn + " is " + Utils.toUtcOffsetString(actualOffsetMillis)
                     + " and not " + Utils.toUtcOffsetString(utcOffsetMillis)
@@ -490,21 +575,20 @@
     }
 
     private static CountryZoneUsage calculateCountryZoneUsage(
-            CountryZonesFile.Country countryIn, Errors processingErrors) {
-        processingErrors.pushScope("Building zone tree");
+            CountryZonesFile.Country countryIn, Errors errors) {
+        errors.pushScope("Building zone tree");
         try {
             CountryZoneTree countryZoneTree = CountryZoneTree.create(
                     countryIn, ZONE_USAGE_CALCS_START, ZONE_USAGE_CALCS_END);
             List<String> countryIssues = countryZoneTree.validateNoPriorityClashes();
             if (!countryIssues.isEmpty()) {
-                processingErrors
-                        .addError("Issues validating country zone trees. Adjust priorities:");
-                countryIssues.forEach(processingErrors::addError);
+                errors.addError("Issues validating country zone trees. Adjust priorities:");
+                countryIssues.forEach(errors::addError);
                 return null;
             }
             return countryZoneTree.calculateCountryZoneUsage(ZONE_USAGE_NOT_AFTER_CUT_OFF);
         } finally {
-            processingErrors.popScope();
+            errors.popScope();
         }
     }
 
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java
index a939ca5..6b960f9 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java
@@ -19,6 +19,7 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -36,7 +37,7 @@
 
     private ZoneTabFile() {}
 
-    static ZoneTabFile parse(String zoneTabFile) throws IOException {
+    static ZoneTabFile parse(String zoneTabFile) throws IOException, ParseException {
         ZoneTabFile zoneTab = new ZoneTabFile();
 
         List<String> lines = Files
@@ -49,9 +50,9 @@
                         .collect(Collectors.toList());
 
         for (String mappingLine : mappingLines) {
-            String[] fields = mappingLine.split("\t");
+            String[] fields = mappingLine.split("\t+");
             if (fields.length < 3) {
-                throw new IOException("Line is malformed: " + mappingLine);
+                throw new ParseException("Line is malformed: " + mappingLine, 0);
             }
             CountryEntry countryEntry = new CountryEntry(fields[0], fields[2]);
             zoneTab.addCountryEntry(countryEntry);
@@ -74,7 +75,10 @@
                     countryEntry.isoCode, k -> new ArrayList<>());
             olsonIds.add(countryEntry.olsonId);
         }
-        return countryIsoToOlsonIdsMap;
+        // Replace each list value with an immutable one.
+        countryIsoToOlsonIdsMap.forEach(
+                (k, v) -> countryIsoToOlsonIdsMap.put(k, Collections.unmodifiableList(v)));
+        return Collections.unmodifiableMap(countryIsoToOlsonIdsMap);
     }
 
     static class CountryEntry {
diff --git a/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto b/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
index 6da1c29..9ed5644 100644
--- a/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
+++ b/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
@@ -35,7 +35,8 @@
 
 message TimeZoneMapping {
     required string id = 1;
-    required string utcOffset = 2;
-    optional bool shownInPicker = 3 [default = true];
-    optional uint32 priority = 4 [default = 1];
+    optional string aliasId = 2;
+    required string utcOffset = 3;
+    optional bool shownInPicker = 4 [default = true];
+    optional uint32 priority = 5 [default = 1];
 }
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/BackwardFileTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/BackwardFileTest.java
new file mode 100644
index 0000000..c575a8e
--- /dev/null
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/BackwardFileTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libcore.timezone.tzlookup;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.libcore.timezone.testing.TestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class BackwardFileTest {
+
+    private Path tempDir;
+
+    @Before
+    public void setUp() throws Exception {
+        tempDir = Files.createTempDirectory("BackwardFileTest");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        TestUtils.deleteDir(tempDir);
+    }
+
+    @Test
+    public void parseEmpty() throws Exception {
+        String file = createFile("");
+        BackwardFile backward = BackwardFile.parse(file);
+        assertTrue(backward.getDirectLinks().isEmpty());
+    }
+
+    @Test
+    public void parseIgnoresCommentsAndEmptyLines() throws Exception {
+        String file = createFile(
+                "# This is a comment",
+                "",
+                "# And another",
+                "Link\tAmerica/Nuuk\t\tAmerica/Godthab"
+        );
+        BackwardFile backward = BackwardFile.parse(file);
+
+        Map<String, String> expectedLinks = new HashMap<>();
+        expectedLinks.put("America/Godthab", "America/Nuuk");
+        assertEquals(expectedLinks, backward.getDirectLinks());
+    }
+
+    @Test
+    public void parse() throws Exception {
+        String file = createFile(
+                "# This is a comment",
+                "Link\tAmerica/Nuuk\t\tAmerica/Godthab",
+                "# This is a comment",
+                "Link\tAfrica/Nairobi\t\tAfrica/Asmera",
+                "# This is a comment",
+                "Link\tAfrica/Abidjan\t\tAfrica/Timbuktu",
+                "# This is a comment"
+        );
+        BackwardFile backward = BackwardFile.parse(file);
+        Map<String, String> expectedLinks = new HashMap<>();
+        expectedLinks.put("America/Godthab", "America/Nuuk");
+        expectedLinks.put("Africa/Asmera", "Africa/Nairobi");
+        expectedLinks.put("Africa/Timbuktu", "Africa/Abidjan");
+
+        assertEquals(expectedLinks, backward.getDirectLinks());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void getLinksWithLoop() throws Exception {
+        String file = createFile(
+                "Link\tAmerica/New_York\t\tAmerica/Los_Angeles",
+                "Link\tAmerica/Los_Angeles\t\tAmerica/Phoenix",
+                "Link\tAmerica/Phoenix\t\tAmerica/New_York"
+        );
+        BackwardFile backward = BackwardFile.parse(file);
+        backward.getDirectLinks();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void parseWithDupes() throws Exception {
+        String file = createFile(
+                "Link\tAmerica/New_York\t\tAmerica/Los_Angeles",
+                "Link\tAmerica/Phoenix\t\tAmerica/Los_Angeles"
+        );
+        BackwardFile.parse(file);
+    }
+
+    @Test(expected = ParseException.class)
+    public void parseMalformedFile() throws Exception {
+        // Mapping lines are expected to have at least three tab-separated columns.
+        String file = createFile("NotLink\tBooHoo");
+        BackwardFile.parse(file);
+    }
+
+    private String createFile(String... lines) throws IOException {
+        return TestUtils.createFile(tempDir, lines);
+    }
+}
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
index 6a4843f..d8677cc 100644
--- a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
@@ -32,7 +32,9 @@
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import static com.android.libcore.timezone.testing.TestUtils.assertAbsent;
@@ -64,10 +66,11 @@
         String countryZonesFile = createFile(tempDir, "THIS IS NOT A VALID FILE");
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
     }
 
@@ -82,11 +85,12 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -100,13 +104,14 @@
                 createValidCountryGb().toBuilder().clearTimeZoneMappings().build();
         CountryZonesFile.CountryZones countryZones = createValidCountryZones(gbWithoutZones);
         String countryZonesFile = createCountryZonesFile(countryZones);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -125,13 +130,14 @@
         CountryZonesFile.CountryZones countryZones =
                 createValidCountryZones(gbWithDuplicateZones);
         String countryZonesFile = createCountryZonesFile(countryZones);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -150,11 +156,12 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -172,11 +179,12 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -194,7 +202,8 @@
                 .clearDefaultTimeZoneId().build();
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
 
-        String tzLookupXml = generateTzLookupXml(gbWithoutDefault, gbZoneTabEntries);
+        String tzLookupXml = generateTzLookupXml(gbWithoutDefault, gbZoneTabEntries,
+                createValidBackwardLinks());
 
         // Check gb's time zone was defaulted.
         assertContains(tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
@@ -211,7 +220,8 @@
                         .build();
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
 
-        String tzLookupXml = generateTzLookupXml(gbWithExplicitDefaultTimeZone, gbZoneTabEntries);
+        String tzLookupXml = generateTzLookupXml(gbWithExplicitDefaultTimeZone, gbZoneTabEntries,
+                createValidBackwardLinks());
 
         // Check gb's time zone was defaulted.
         assertContains(tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
@@ -229,11 +239,12 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -251,11 +262,12 @@
 
         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -271,11 +283,11 @@
 
         String zoneTabFile =
                 createZoneTabFile(createValidZoneTabEntriesFr(), createValidZoneTabEntriesUs());
-
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -293,11 +305,11 @@
         String countryZonesFile = createCountryZonesFile(countryZones);
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
-
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -313,10 +325,11 @@
         String zoneTabFileWithDupes = createZoneTabFile(
                 createValidZoneTabEntriesGb(), createValidZoneTabEntriesGb());
 
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
-        TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFileWithDupes, outputFile);
+        TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+                countryZonesFile, zoneTabFileWithDupes, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -334,11 +347,12 @@
         String countryZonesFile = createCountryZonesFile(countryZones);
 
         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -360,11 +374,12 @@
                 new ArrayList<>(createValidZoneTabEntriesGb());
         zoneTabEntriesWithBadId.add(new ZoneTabFile.CountryEntry("GB", INVALID_TIME_ZONE_ID));
         String zoneTabFile = createZoneTabFile(zoneTabEntriesWithBadId);
+        String backwardFile = createBackwardFile(createValidBackwardLinks());
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertFalse(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -372,9 +387,121 @@
     }
 
     @Test
+    public void badBackwardFile() throws Exception {
+        CountryZonesFile.CountryZones countryZones = createValidCountryZones(createValidCountryGb());
+        String countryZonesFile = createCountryZonesFile(countryZones);
+        String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
+
+        String badBackwardFile = TestUtils.createFile(tempDir, "THIS IS NOT VALID");
+
+        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+
+        TzLookupGenerator tzLookupGenerator =
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, badBackwardFile, outputFile);
+        assertFalse(tzLookupGenerator.execute());
+
+        Path outputFilePath = Paths.get(outputFile);
+        assertEquals(0, Files.size(outputFilePath));
+    }
+
+    @Test
+    public void usingOldLinksValid() throws Exception {
+        // This simulates a case where America/Godthab has been superseded by America/Nuuk in IANA
+        // data, but Android wants to continue using America/Godthab.
+        String countryZonesWithOldIdText =
+                "isoCode:\"gl\"\n"
+                + "defaultTimeZoneId:\"America/Godthab\"\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"0:00\"\n"
+                + "  id:\"America/Danmarkshavn\"\n"
+                + ">\n"
+                + "\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"-1:00\"\n"
+                + "  id:\"America/Scoresbysund\"\n"
+                + ">\n"
+                + "\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"-3:00\"\n"
+                + "  id:\"America/Godthab\"\n"
+                + "  aliasId:\"America/Nuuk\"\n"
+                + ">\n"
+                + "\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"-4:00\"\n"
+                + "  id:\"America/Thule\"\n"
+                + ">\n";
+        Country country = parseCountry(countryZonesWithOldIdText);
+        List<ZoneTabFile.CountryEntry> zoneTabWithNewIds = Arrays.asList(
+                new ZoneTabFile.CountryEntry("GL", "America/Nuuk"),
+                new ZoneTabFile.CountryEntry("GL", "America/Danmarkshavn"),
+                new ZoneTabFile.CountryEntry("GL", "America/Scoresbysund"),
+                new ZoneTabFile.CountryEntry("GL", "America/Thule")
+        );
+        Map<String, String> links = new HashMap<>();
+        links.put("America/Godthab", "America/Nuuk");
+
+        String tzLookupXml = generateTzLookupXml(country, zoneTabWithNewIds, links);
+
+        String expectedOutput =
+                "<id>America/Danmarkshavn</id>\n"
+                        + "<id>America/Scoresbysund</id>\n"
+                        + "<id>America/Godthab</id>\n"
+                        + "<id>America/Thule</id>\n";
+        String[] expectedLines = expectedOutput.split("\\n");
+        for (String expectedLine : expectedLines) {
+            assertContains(tzLookupXml, expectedLine);
+        }
+    }
+
+    @Test
+    public void usingOldLinksMissingAlias() throws Exception {
+        // This simulates a case where America/Godthab has been superseded by America/Nuuk in IANA
+        // data, but the Android file hasn't been updated properly.
+        String countryZonesWithOldIdText =
+                "isoCode:\"gl\"\n"
+                + "defaultTimeZoneId:\"America/Godthab\"\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"0:00\"\n"
+                + "  id:\"America/Danmarkshavn\"\n"
+                + ">\n"
+                + "\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"-1:00\"\n"
+                + "  id:\"America/Scoresbysund\"\n"
+                + ">\n"
+                + "\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"-3:00\"\n"
+                + "  id:\"America/Godthab\"\n"
+
+                // Exclude the crucial line that tells the generator we meant to use an old ID...
+                /* + "  aliasId:\"America/Nuuk\"\n" */
+
+                + ">\n"
+                + "\n"
+                + "timeZoneMappings:<\n"
+                + "  utcOffset:\"-4:00\"\n"
+                + "  id:\"America/Thule\"\n"
+                + ">\n";
+        Country country = parseCountry(countryZonesWithOldIdText);
+        List<ZoneTabFile.CountryEntry> zoneTabWithNewIds = Arrays.asList(
+                new ZoneTabFile.CountryEntry("GL", "America/Nuuk"),
+                new ZoneTabFile.CountryEntry("GL", "America/Danmarkshavn"),
+                new ZoneTabFile.CountryEntry("GL", "America/Scoresbysund"),
+                new ZoneTabFile.CountryEntry("GL", "America/Thule")
+        );
+        Map<String, String> links = new HashMap<>();
+        links.put("America/Godthab", "America/Nuuk");
+
+        generateTzLookupXmlExpectFailure(country, zoneTabWithNewIds, links);
+    }
+
+    @Test
     public void everUtc_true() throws Exception {
         CountryZonesFile.Country validCountryGb = createValidCountryGb();
-        String tzLookupXml = generateTzLookupXml(validCountryGb, createValidZoneTabEntriesGb());
+        String tzLookupXml = generateTzLookupXml(validCountryGb, createValidZoneTabEntriesGb(),
+                createValidBackwardLinks());
 
         // Check gb's entry contains everutc="y".
         assertContains(tzLookupXml, "everutc=\"y\"");
@@ -383,7 +510,8 @@
     @Test
     public void everUtc_false() throws Exception {
         CountryZonesFile.Country validCountryFr = createValidCountryFr();
-        String tzLookupXml = generateTzLookupXml(validCountryFr, createValidZoneTabEntriesFr());
+        String tzLookupXml = generateTzLookupXml(validCountryFr, createValidZoneTabEntriesFr(),
+                createValidBackwardLinks());
 
         // Check fr's entry contains everutc="n".
         assertContains(tzLookupXml, "everutc=\"n\"");
@@ -401,7 +529,8 @@
         countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
         CountryZonesFile.Country country = countryBuilder.build();
 
-        String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr());
+        String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr(),
+                createValidBackwardLinks());
 
         assertContains(tzLookupXml, "picker=\"n\"");
     }
@@ -418,7 +547,8 @@
         countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
         CountryZonesFile.Country country = countryBuilder.build();
 
-        String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr());
+        String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr(),
+                createValidBackwardLinks());
 
         // We should not see anything "picker="y" is the implicit default.
         assertAbsent(tzLookupXml, "picker=");
@@ -428,7 +558,8 @@
     public void notAfter() throws Exception {
         CountryZonesFile.Country country = createValidCountryUs();
         List<ZoneTabFile.CountryEntry> zoneTabEntries = createValidZoneTabEntriesUs();
-        String tzLookupXml = generateTzLookupXml(country, zoneTabEntries);
+        String tzLookupXml = generateTzLookupXml(country, zoneTabEntries,
+                createValidBackwardLinks());
         String expectedOutput =
                 "<id>America/New_York</id>\n"
                 + "<id notafter=\"167814000000\">America/Detroit</id>\n"
@@ -466,17 +597,19 @@
     }
 
     private String generateTzLookupXml(CountryZonesFile.Country country,
-            List<ZoneTabFile.CountryEntry> zoneTabEntries) throws Exception {
+            List<ZoneTabFile.CountryEntry> zoneTabEntries, Map<String, String> backwardLinks)
+            throws Exception {
 
         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
         String countryZonesFile = createCountryZonesFile(countryZones);
 
         String zoneTabFile = createZoneTabFile(zoneTabEntries);
+        String backwardFile = createBackwardFile(backwardLinks);
 
         String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
 
         TzLookupGenerator tzLookupGenerator =
-                new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
         assertTrue(tzLookupGenerator.execute());
 
         Path outputFilePath = Paths.get(outputFile);
@@ -485,6 +618,23 @@
         return readFileToString(outputFilePath);
     }
 
+    private void generateTzLookupXmlExpectFailure(CountryZonesFile.Country country,
+            List<ZoneTabFile.CountryEntry> zoneTabEntries, Map<String, String> backwardLinks)
+            throws Exception {
+
+        CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
+        String countryZonesFile = createCountryZonesFile(countryZones);
+
+        String zoneTabFile = createZoneTabFile(zoneTabEntries);
+        String backwardFile = createBackwardFile(backwardLinks);
+
+        String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+
+        TzLookupGenerator tzLookupGenerator =
+                new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+        assertFalse(tzLookupGenerator.execute());
+    }
+
     private static String readFileToString(Path file) throws IOException {
         return new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
     }
@@ -709,6 +859,19 @@
                 new ZoneTabFile.CountryEntry("FR", "Europe/Paris"));
     }
 
+    private String createBackwardFile(Map<String, String> links) throws Exception {
+        List<String> lines = links.entrySet().stream()
+                .map(x -> "Link\t" + x.getValue() + "\t\t" + x.getKey())
+                .collect(Collectors.toList());
+        return TestUtils.createFile(tempDir, lines.toArray(new String[0]));
+    }
+
+    private static Map<String, String> createValidBackwardLinks() {
+        Map<String, String> map = new HashMap<>();
+        map.put("America/Godthab", "America/Nuuk");
+        return map;
+    }
+
     private static Country parseCountry(String text) throws Exception {
         Country.Builder builder = Country.newBuilder();
         TextFormat.getParser().merge(text, builder);
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java
index b581ffd..108b902 100644
--- a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java
@@ -25,6 +25,7 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.text.ParseException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -32,7 +33,6 @@
 
 import static junit.framework.TestCase.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 public class ZoneTabFileTest {
 
@@ -75,7 +75,7 @@
                 "# This is a comment",
                 "GB\tStuff\tEurope/London\tStuff",
                 "# This is a comment",
-                "US\tStuff\tAmerica/New_York\tStuff",
+                "US\tStuff\t\tAmerica/New_York\tStuff",
                 "# This is a comment",
                 "US\tStuff\tAmerica/Los_Angeles",
                 "# This is a comment"
@@ -89,14 +89,11 @@
                 zoneTab.getCountryEntries());
     }
 
-    @Test
+    @Test(expected = ParseException.class)
     public void parseMalformedFile() throws Exception {
         // Mapping lines are expected to have at least three tab-separated columns.
         String file = createFile("GB\tStuff");
-        try {
-            ZoneTabFile.parse(file);
-            fail();
-        } catch (IOException expected) {}
+        ZoneTabFile.parse(file);
     }
 
     @Test
diff --git a/update-tzdata.py b/update-tzdata.py
index 61ab30e..ee370c1 100755
--- a/update-tzdata.py
+++ b/update-tzdata.py
@@ -184,8 +184,10 @@
   tzdatautil.InvokeSoong(android_build_top, ['tzlookup_generator'])
 
   zone_tab_file = '%s/zone.tab' % iana_data_dir
+  backward_file = '%s/backward' % iana_data_dir
   command = '%s/bin/tzlookup_generator' % android_host_out
-  subprocess.check_call([command, countryzones_source_file, zone_tab_file, tzlookup_dest_file])
+  subprocess.check_call([command, countryzones_source_file, zone_tab_file, backward_file,
+                         tzlookup_dest_file])
 
 
 def BuildTelephonylookup():