• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 libcore.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  * Constants and logic associated with the time zone data version file.
29  * @hide
30  */
31 @libcore.api.CorePlatformApi
32 public class TzDataSetVersion {
33 
34     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
35     /**
36      * The name typically given to the {@link TzDataSetVersion} file. See
37      * {@link TzDataSetVersion#readFromFile(File)}.
38      */
39     @libcore.api.CorePlatformApi
40     public static final String DEFAULT_FILE_NAME = "tz_version";
41 
42     /**
43      * The major tz data format version supported by this device.
44      * Increment this for non-backwards compatible changes to the tz data format. Reset the minor
45      * version to 1 when doing so.
46      */
47     // @VisibleForTesting : Keep this inline-able: it is used from CTS tests.
48     public static final int CURRENT_FORMAT_MAJOR_VERSION = 3; // Android Q
49 
50     /**
51      * Returns the major tz data format version supported by this device.
52      */
53     @libcore.api.CorePlatformApi
currentFormatMajorVersion()54     public static int currentFormatMajorVersion() {
55         return CURRENT_FORMAT_MAJOR_VERSION;
56     }
57 
58     /**
59      * The minor tz data format version supported by this device. Increment this for
60      * backwards-compatible changes to the tz data format.
61      */
62     // @VisibleForTesting : Keep this inline-able: it is used from CTS tests.
63     public static final int CURRENT_FORMAT_MINOR_VERSION = 1;
64 
65     /**
66      * Returns the minor tz data format version supported by this device.
67      */
68     @libcore.api.CorePlatformApi
currentFormatMinorVersion()69     public static int currentFormatMinorVersion() {
70         return CURRENT_FORMAT_MINOR_VERSION;
71     }
72 
73     /** The full major + minor tz data format version for this device. */
74     private static final String FULL_CURRENT_FORMAT_VERSION_STRING =
75             toFormatVersionString(CURRENT_FORMAT_MAJOR_VERSION, CURRENT_FORMAT_MINOR_VERSION);
76 
77     private static final int FORMAT_VERSION_STRING_LENGTH =
78             FULL_CURRENT_FORMAT_VERSION_STRING.length();
79     private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})");
80 
81     /** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */
82     private static final Pattern RULES_VERSION_PATTERN = Pattern.compile("(\\d{4}\\w)");
83 
84     private static final int RULES_VERSION_LENGTH = 5;
85 
86     /** A pattern that matches the revision of a rules update. e.g. "001" */
87     private static final Pattern REVISION_PATTERN = Pattern.compile("(\\d{3})");
88 
89     private static final int REVISION_LENGTH = 3;
90 
91     /**
92      * The length of a well-formed tz data set version file:
93      * {Format version}|{Rule version}|{Revision}
94      */
95     private static final int TZ_DATA_VERSION_FILE_LENGTH = FORMAT_VERSION_STRING_LENGTH + 1
96             + RULES_VERSION_LENGTH
97             + 1 + REVISION_LENGTH;
98 
99     private static final Pattern TZ_DATA_VERSION_FILE_PATTERN = Pattern.compile(
100             FORMAT_VERSION_PATTERN.pattern() + "\\|"
101                     + RULES_VERSION_PATTERN.pattern() + "\\|"
102                     + REVISION_PATTERN.pattern()
103                     + ".*" /* ignore trailing */);
104 
105     public final int formatMajorVersion;
106     public final int formatMinorVersion;
107 
108     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
109     @libcore.api.CorePlatformApi
110     public final String rulesVersion;
111 
112     public final int revision;
113 
114     @libcore.api.CorePlatformApi
TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion, int revision)115     public TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion,
116             int revision) throws TzDataSetException {
117         this.formatMajorVersion = validate3DigitVersion(formatMajorVersion);
118         this.formatMinorVersion = validate3DigitVersion(formatMinorVersion);
119         if (!RULES_VERSION_PATTERN.matcher(rulesVersion).matches()) {
120             throw new TzDataSetException("Invalid rulesVersion: " + rulesVersion);
121         }
122         this.rulesVersion = rulesVersion;
123         this.revision = validate3DigitVersion(revision);
124     }
125 
126     // VisibleForTesting
fromBytes(byte[] bytes)127     public static TzDataSetVersion fromBytes(byte[] bytes) throws TzDataSetException {
128         String tzDataVersion = new String(bytes, StandardCharsets.US_ASCII);
129         try {
130             Matcher matcher = TZ_DATA_VERSION_FILE_PATTERN.matcher(tzDataVersion);
131             if (!matcher.matches()) {
132                 throw new TzDataSetException(
133                         "Invalid tz data version string: \"" + tzDataVersion + "\"");
134             }
135             String formatMajorVersion = matcher.group(1);
136             String formatMinorVersion = matcher.group(2);
137             String rulesVersion = matcher.group(3);
138             String revision = matcher.group(4);
139             return new TzDataSetVersion(
140                     from3DigitVersionString(formatMajorVersion),
141                     from3DigitVersionString(formatMinorVersion),
142                     rulesVersion,
143                     from3DigitVersionString(revision));
144         } catch (IndexOutOfBoundsException e) {
145             // The use of the regexp above should make this impossible.
146             throw new TzDataSetException(
147                     "tz data version string too short: \"" + tzDataVersion + "\"");
148         }
149     }
150 
151     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
152     @libcore.api.CorePlatformApi
readFromFile(File file)153     public static TzDataSetVersion readFromFile(File file) throws IOException, TzDataSetException {
154         byte[] versionBytes = readBytes(file, TzDataSetVersion.TZ_DATA_VERSION_FILE_LENGTH);
155         return fromBytes(versionBytes);
156     }
157 
158     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
159     @libcore.api.CorePlatformApi
toBytes()160     public byte[] toBytes() {
161         return toBytes(formatMajorVersion, formatMinorVersion, rulesVersion, revision);
162     }
163 
toBytes( int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision)164     private static byte[] toBytes(
165             int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision) {
166         return (toFormatVersionString(majorFormatVersion, minorFormatVerison)
167                 + "|" + rulesVersion + "|" + to3DigitVersionString(revision))
168                 .getBytes(StandardCharsets.US_ASCII);
169     }
170 
171     @libcore.api.CorePlatformApi
isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion)172     public static boolean isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion) {
173         return (CURRENT_FORMAT_MAJOR_VERSION == tzDataVersion.formatMajorVersion)
174                 && (CURRENT_FORMAT_MINOR_VERSION <= tzDataVersion.formatMinorVersion);
175     }
176 
177     @Override
equals(Object o)178     public boolean equals(Object o) {
179         if (this == o) {
180             return true;
181         }
182         if (o == null || getClass() != o.getClass()) {
183             return false;
184         }
185 
186         TzDataSetVersion that = (TzDataSetVersion) o;
187 
188         if (formatMajorVersion != that.formatMajorVersion) {
189             return false;
190         }
191         if (formatMinorVersion != that.formatMinorVersion) {
192             return false;
193         }
194         if (revision != that.revision) {
195             return false;
196         }
197         return rulesVersion.equals(that.rulesVersion);
198     }
199 
200     @Override
hashCode()201     public int hashCode() {
202         int result = formatMajorVersion;
203         result = 31 * result + formatMinorVersion;
204         result = 31 * result + rulesVersion.hashCode();
205         result = 31 * result + revision;
206         return result;
207     }
208 
209     @Override
toString()210     public String toString() {
211         return "TzDataSetVersion{" +
212                 "formatMajorVersion=" + formatMajorVersion +
213                 ", formatMinorVersion=" + formatMinorVersion +
214                 ", rulesVersion='" + rulesVersion + '\'' +
215                 ", revision=" + revision +
216                 '}';
217     }
218 
219     /**
220      * Returns a version as a zero-padded three-digit String value.
221      */
to3DigitVersionString(int version)222     private static String to3DigitVersionString(int version) {
223         try {
224             return String.format(Locale.ROOT, "%03d", validate3DigitVersion(version));
225         } catch (TzDataSetException e) {
226             throw new IllegalArgumentException(e);
227         }
228     }
229 
230     /**
231      * Validates and parses a zero-padded three-digit String value.
232      */
from3DigitVersionString(String versionString)233     private static int from3DigitVersionString(String versionString) throws TzDataSetException {
234         final String parseErrorMessage = "versionString must be a zero padded, 3 digit, positive"
235                 + " decimal integer";
236         if (versionString.length() != 3) {
237             throw new TzDataSetException(parseErrorMessage);
238         }
239         try {
240             int version = Integer.parseInt(versionString);
241             return validate3DigitVersion(version);
242         } catch (NumberFormatException e) {
243             throw new TzDataSetException(parseErrorMessage, e);
244         }
245     }
246 
validate3DigitVersion(int value)247     private static int validate3DigitVersion(int value) throws TzDataSetException {
248         // 0 is allowed but is reserved for testing.
249         if (value < 0 || value > 999) {
250             throw new TzDataSetException("Expected 0 <= value <= 999, was " + value);
251         }
252         return value;
253     }
254 
toFormatVersionString(int majorFormatVersion, int minorFormatVersion)255     private static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) {
256         return to3DigitVersionString(majorFormatVersion)
257                 + "." + to3DigitVersionString(minorFormatVersion);
258     }
259 
260     /**
261      * Reads up to {@code maxBytes} bytes from the specified file. The returned array can be
262      * shorter than {@code maxBytes} if the file is shorter.
263      */
readBytes(File file, int maxBytes)264     private static byte[] readBytes(File file, int maxBytes) throws IOException {
265         if (maxBytes <= 0) {
266             throw new IllegalArgumentException("maxBytes ==" + maxBytes);
267         }
268 
269         try (FileInputStream in = new FileInputStream(file)) {
270             byte[] max = new byte[maxBytes];
271             int bytesRead = in.read(max, 0, maxBytes);
272             byte[] toReturn = new byte[bytesRead];
273             System.arraycopy(max, 0, toReturn, 0, bytesRead);
274             return toReturn;
275         }
276     }
277 
278     /**
279      * A checked exception used in connection with time zone data sets.
280      */
281     @libcore.api.CorePlatformApi
282     public static class TzDataSetException extends Exception {
283 
284         @libcore.api.CorePlatformApi
TzDataSetException(String message)285         public TzDataSetException(String message) {
286             super(message);
287         }
288 
289         @libcore.api.CorePlatformApi
TzDataSetException(String message, Throwable cause)290         public TzDataSetException(String message, Throwable cause) {
291             super(message, cause);
292         }
293     }
294 }
295