1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.i18n.timezone;
18 
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.IOException;
22 import java.nio.charset.StandardCharsets;
23 import java.util.Locale;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26 
27 /**
28  * Version information associated with the set of time zone data on a device.
29  *
30  * <p>Time Zone Data Sets have a major ({@link #getFormatMajorVersion()}) and minor
31  * ({@link #currentFormatMinorVersion()}) version number:
32  * <ul>
33  *   <li>Major version numbers are mutually incompatible. e.g. v2 is not compatible with a v1 or a
34  *   v3 device.</li>
35  *   <li>Minor version numbers are backwards compatible. e.g. a v2.2 data set will work
36  *   on a v2.1 device but not a v2.3 device. The minor version is reset to 1 when the major version
37  *   is incremented.</li>
38  * </ul>
39  *
40  * <p>Data sets contain time zone rules and other data associated wtih a tzdb release
41  * ({@link #getRulesVersion()}) and an additional Android-specific revision number
42  * ({@link #getRevision()}).
43  *
44  * <p>See platform/system/timezone/README.android for more information.
45  * @hide
46  */
47 @libcore.api.CorePlatformApi
48 public final class TzDataSetVersion {
49 
50     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
51     /**
52      * The name typically given to the {@link TzDataSetVersion} file. See
53      * {@link TzDataSetVersion#readFromFile(File)}.
54      */
55     @libcore.api.CorePlatformApi
56     public static final String DEFAULT_FILE_NAME = "tz_version";
57 
58     /**
59      * The major tz data format version supported by this device.
60      * Increment this for non-backwards compatible changes to the tz data format. Reset the minor
61      * version to 1 when doing so.
62      */
63     // @VisibleForTesting : Keep this inline-able: it is used from CTS tests.
64     public static final int CURRENT_FORMAT_MAJOR_VERSION = 5; // Android S
65 
66     /**
67      * Returns the major tz data format version supported by this device.
68      */
69     @libcore.api.CorePlatformApi
currentFormatMajorVersion()70     public static int currentFormatMajorVersion() {
71         return CURRENT_FORMAT_MAJOR_VERSION;
72     }
73 
74     /**
75      * The minor tz data format version supported by this device. Increment this for
76      * backwards-compatible changes to the tz data format.
77      */
78     // @VisibleForTesting : Keep this inline-able: it is used from CTS tests.
79     public static final int CURRENT_FORMAT_MINOR_VERSION = 1;
80 
81     /**
82      * Returns the minor tz data format version supported by this device.
83      */
84     @libcore.api.CorePlatformApi
currentFormatMinorVersion()85     public static int currentFormatMinorVersion() {
86         return CURRENT_FORMAT_MINOR_VERSION;
87     }
88 
89     /** The full major + minor tz data format version for this device. */
90     private static final String FULL_CURRENT_FORMAT_VERSION_STRING =
91             toFormatVersionString(CURRENT_FORMAT_MAJOR_VERSION, CURRENT_FORMAT_MINOR_VERSION);
92 
93     private static final int FORMAT_VERSION_STRING_LENGTH =
94             FULL_CURRENT_FORMAT_VERSION_STRING.length();
95     private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})");
96 
97     /** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */
98     private static final Pattern RULES_VERSION_PATTERN = Pattern.compile("(\\d{4}\\w)");
99 
100     private static final int RULES_VERSION_LENGTH = 5;
101 
102     /** A pattern that matches the revision of a rules update. e.g. "001" */
103     private static final Pattern REVISION_PATTERN = Pattern.compile("(\\d{3})");
104 
105     private static final int REVISION_LENGTH = 3;
106 
107     /**
108      * The length of a well-formed tz data set version file:
109      * {Format version}|{Rule version}|{Revision}
110      */
111     private static final int TZ_DATA_VERSION_FILE_LENGTH = FORMAT_VERSION_STRING_LENGTH + 1
112             + RULES_VERSION_LENGTH
113             + 1 + REVISION_LENGTH;
114 
115     private static final Pattern TZ_DATA_VERSION_FILE_PATTERN = Pattern.compile(
116             FORMAT_VERSION_PATTERN.pattern() + "\\|"
117                     + RULES_VERSION_PATTERN.pattern() + "\\|"
118                     + REVISION_PATTERN.pattern()
119                     + ".*" /* ignore trailing */);
120 
121     private final int formatMajorVersion;
122     private final int formatMinorVersion;
123     private final String rulesVersion;
124     private final int revision;
125 
126     @libcore.api.CorePlatformApi
TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion, int revision)127     public TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion,
128             int revision) throws TzDataSetException {
129         this.formatMajorVersion = validate3DigitVersion(formatMajorVersion);
130         this.formatMinorVersion = validate3DigitVersion(formatMinorVersion);
131         if (!RULES_VERSION_PATTERN.matcher(rulesVersion).matches()) {
132             throw new TzDataSetException("Invalid rulesVersion: " + rulesVersion);
133         }
134         this.rulesVersion = rulesVersion;
135         this.revision = validate3DigitVersion(revision);
136     }
137 
138     // VisibleForTesting
fromBytes(byte[] bytes)139     public static TzDataSetVersion fromBytes(byte[] bytes) throws TzDataSetException {
140         String tzDataVersion = new String(bytes, StandardCharsets.US_ASCII);
141         try {
142             Matcher matcher = TZ_DATA_VERSION_FILE_PATTERN.matcher(tzDataVersion);
143             if (!matcher.matches()) {
144                 throw new TzDataSetException(
145                         "Invalid tz data version string: \"" + tzDataVersion + "\"");
146             }
147             String formatMajorVersion = matcher.group(1);
148             String formatMinorVersion = matcher.group(2);
149             String rulesVersion = matcher.group(3);
150             String revision = matcher.group(4);
151             return new TzDataSetVersion(
152                     from3DigitVersionString(formatMajorVersion),
153                     from3DigitVersionString(formatMinorVersion),
154                     rulesVersion,
155                     from3DigitVersionString(revision));
156         } catch (IndexOutOfBoundsException e) {
157             // The use of the regexp above should make this impossible.
158             throw new TzDataSetException(
159                     "tz data version string too short: \"" + tzDataVersion + "\"");
160         }
161     }
162 
163     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
164     @libcore.api.CorePlatformApi
readFromFile(File file)165     public static TzDataSetVersion readFromFile(File file) throws IOException, TzDataSetException {
166         byte[] versionBytes = readBytes(file, TzDataSetVersion.TZ_DATA_VERSION_FILE_LENGTH);
167         return fromBytes(versionBytes);
168     }
169 
170     /** Returns the major version number. See {@link TzDataSetVersion}. */
171     @libcore.api.CorePlatformApi
getFormatMajorVersion()172     public int getFormatMajorVersion() {
173         return formatMajorVersion;
174     }
175 
176     /** Returns the minor version number. See {@link TzDataSetVersion}. */
177     @libcore.api.CorePlatformApi
getFormatMinorVersion()178     public int getFormatMinorVersion() {
179         return formatMinorVersion;
180     }
181 
182     /** Returns the tzdb version string. See {@link TzDataSetVersion}. */
183     @libcore.api.CorePlatformApi
getRulesVersion()184     public String getRulesVersion() {
185         return rulesVersion;
186     }
187 
188     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
189     /** Returns the Android revision. See {@link TzDataSetVersion}. */
190     @libcore.api.CorePlatformApi
getRevision()191     public int getRevision() {
192         return revision;
193     }
194 
195     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
196     @libcore.api.CorePlatformApi
toBytes()197     public byte[] toBytes() {
198         return toBytes(formatMajorVersion, formatMinorVersion, rulesVersion, revision);
199     }
200 
toBytes( int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision)201     private static byte[] toBytes(
202             int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision) {
203         return (toFormatVersionString(majorFormatVersion, minorFormatVerison)
204                 + "|" + rulesVersion + "|" + to3DigitVersionString(revision))
205                 .getBytes(StandardCharsets.US_ASCII);
206     }
207 
208     @libcore.api.CorePlatformApi
isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion)209     public static boolean isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion) {
210         return (CURRENT_FORMAT_MAJOR_VERSION == tzDataVersion.formatMajorVersion)
211                 && (CURRENT_FORMAT_MINOR_VERSION <= tzDataVersion.formatMinorVersion);
212     }
213 
214     @Override
equals(Object o)215     public boolean equals(Object o) {
216         if (this == o) {
217             return true;
218         }
219         if (o == null || getClass() != o.getClass()) {
220             return false;
221         }
222         TzDataSetVersion that = (TzDataSetVersion) o;
223         if (formatMajorVersion != that.formatMajorVersion) {
224             return false;
225         }
226         if (formatMinorVersion != that.formatMinorVersion) {
227             return false;
228         }
229         if (revision != that.revision) {
230             return false;
231         }
232         return rulesVersion.equals(that.rulesVersion);
233     }
234 
235     @Override
hashCode()236     public int hashCode() {
237         int result = formatMajorVersion;
238         result = 31 * result + formatMinorVersion;
239         result = 31 * result + rulesVersion.hashCode();
240         result = 31 * result + revision;
241         return result;
242     }
243 
244     @Override
toString()245     public String toString() {
246         return "TzDataSetVersion{" +
247                 "formatMajorVersion=" + formatMajorVersion +
248                 ", formatMinorVersion=" + formatMinorVersion +
249                 ", rulesVersion='" + rulesVersion + '\'' +
250                 ", revision=" + revision +
251                 '}';
252     }
253 
254     /**
255      * Returns a version as a zero-padded three-digit String value.
256      */
to3DigitVersionString(int version)257     private static String to3DigitVersionString(int version) {
258         try {
259             return String.format(Locale.ROOT, "%03d", validate3DigitVersion(version));
260         } catch (TzDataSetException e) {
261             throw new IllegalArgumentException(e);
262         }
263     }
264 
265     /**
266      * Validates and parses a zero-padded three-digit String value.
267      */
from3DigitVersionString(String versionString)268     private static int from3DigitVersionString(String versionString) throws TzDataSetException {
269         final String parseErrorMessage = "versionString must be a zero padded, 3 digit, positive"
270                 + " decimal integer";
271         if (versionString.length() != 3) {
272             throw new TzDataSetException(parseErrorMessage);
273         }
274         try {
275             int version = Integer.parseInt(versionString);
276             return validate3DigitVersion(version);
277         } catch (NumberFormatException e) {
278             throw new TzDataSetException(parseErrorMessage, e);
279         }
280     }
281 
validate3DigitVersion(int value)282     private static int validate3DigitVersion(int value) throws TzDataSetException {
283         // 0 is allowed but is reserved for testing.
284         if (value < 0 || value > 999) {
285             throw new TzDataSetException("Expected 0 <= value <= 999, was " + value);
286         }
287         return value;
288     }
289 
toFormatVersionString(int majorFormatVersion, int minorFormatVersion)290     private static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) {
291         return to3DigitVersionString(majorFormatVersion)
292                 + "." + to3DigitVersionString(minorFormatVersion);
293     }
294 
295     /**
296      * Reads up to {@code maxBytes} bytes from the specified file. The returned array can be
297      * shorter than {@code maxBytes} if the file is shorter.
298      */
readBytes(File file, int maxBytes)299     private static byte[] readBytes(File file, int maxBytes) throws IOException {
300         if (maxBytes <= 0) {
301             throw new IllegalArgumentException("maxBytes ==" + maxBytes);
302         }
303 
304         try (FileInputStream in = new FileInputStream(file)) {
305             byte[] max = new byte[maxBytes];
306             int bytesRead = in.read(max, 0, maxBytes);
307             byte[] toReturn = new byte[bytesRead];
308             System.arraycopy(max, 0, toReturn, 0, bytesRead);
309             return toReturn;
310         }
311     }
312 
313     /**
314      * A checked exception used in connection with time zone data sets.
315      */
316     @libcore.api.CorePlatformApi
317     public static class TzDataSetException extends Exception {
318 
319         @libcore.api.CorePlatformApi
TzDataSetException(String message)320         public TzDataSetException(String message) {
321             super(message);
322         }
323 
324         @libcore.api.CorePlatformApi
TzDataSetException(String message, Throwable cause)325         public TzDataSetException(String message, Throwable cause) {
326             super(message, cause);
327         }
328     }
329 }
330