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 // LINT.IfChange 65 public static final int CURRENT_FORMAT_MAJOR_VERSION = 8; // Android V 66 // LINT.ThenChange(external/icu/android_icu4j/src/main/java/android/icu/platform/AndroidDataFiles.java) 67 68 /** 69 * Returns the major tz data format version supported by this device. 70 */ 71 @libcore.api.CorePlatformApi currentFormatMajorVersion()72 public static int currentFormatMajorVersion() { 73 return CURRENT_FORMAT_MAJOR_VERSION; 74 } 75 76 /** 77 * The minor tz data format version supported by this device. Increment this for 78 * backwards-compatible changes to the tz data format. 79 */ 80 // @VisibleForTesting : Keep this inline-able: it is used from CTS tests. 81 public static final int CURRENT_FORMAT_MINOR_VERSION = 1; 82 83 /** 84 * Returns the minor tz data format version supported by this device. 85 */ 86 @libcore.api.CorePlatformApi currentFormatMinorVersion()87 public static int currentFormatMinorVersion() { 88 return CURRENT_FORMAT_MINOR_VERSION; 89 } 90 91 /** The full major + minor tz data format version's length for this device. */ 92 // @VisibleForTesting 93 public static final int FORMAT_VERSION_STRING_LENGTH = 7; 94 private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})"); 95 96 /** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */ 97 private static final Pattern RULES_VERSION_PATTERN = Pattern.compile("(\\d{4}\\w)"); 98 99 private static final int RULES_VERSION_LENGTH = 5; 100 101 /** A pattern that matches the revision of a rules update. e.g. "001" */ 102 private static final Pattern REVISION_PATTERN = Pattern.compile("(\\d{3})"); 103 104 private static final int REVISION_LENGTH = 3; 105 106 /** 107 * The length of a well-formed tz data set version file: 108 * {Format version}|{Rule version}|{Revision} 109 */ 110 private static final int TZ_DATA_VERSION_FILE_LENGTH = FORMAT_VERSION_STRING_LENGTH + 1 111 + RULES_VERSION_LENGTH 112 + 1 + REVISION_LENGTH; 113 114 private static final Pattern TZ_DATA_VERSION_FILE_PATTERN = Pattern.compile( 115 FORMAT_VERSION_PATTERN.pattern() + "\\|" 116 + RULES_VERSION_PATTERN.pattern() + "\\|" 117 + REVISION_PATTERN.pattern() 118 + ".*" /* ignore trailing */); 119 120 private final int formatMajorVersion; 121 private final int formatMinorVersion; 122 private final String rulesVersion; 123 private final int revision; 124 125 @libcore.api.CorePlatformApi TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion, int revision)126 public TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion, 127 int revision) throws TzDataSetException { 128 this.formatMajorVersion = validate3DigitVersion(formatMajorVersion); 129 this.formatMinorVersion = validate3DigitVersion(formatMinorVersion); 130 if (!RULES_VERSION_PATTERN.matcher(rulesVersion).matches()) { 131 throw new TzDataSetException("Invalid rulesVersion: " + rulesVersion); 132 } 133 this.rulesVersion = rulesVersion; 134 this.revision = validate3DigitVersion(revision); 135 } 136 137 // VisibleForTesting fromBytes(byte[] bytes)138 public static TzDataSetVersion fromBytes(byte[] bytes) throws TzDataSetException { 139 String tzDataVersion = new String(bytes, StandardCharsets.US_ASCII); 140 try { 141 Matcher matcher = TZ_DATA_VERSION_FILE_PATTERN.matcher(tzDataVersion); 142 if (!matcher.matches()) { 143 throw new TzDataSetException( 144 "Invalid tz data version string: \"" + tzDataVersion + "\""); 145 } 146 String formatMajorVersion = matcher.group(1); 147 String formatMinorVersion = matcher.group(2); 148 String rulesVersion = matcher.group(3); 149 String revision = matcher.group(4); 150 return new TzDataSetVersion( 151 from3DigitVersionString(formatMajorVersion), 152 from3DigitVersionString(formatMinorVersion), 153 rulesVersion, 154 from3DigitVersionString(revision)); 155 } catch (IndexOutOfBoundsException e) { 156 // The use of the regexp above should make this impossible. 157 throw new TzDataSetException( 158 "tz data version string too short: \"" + tzDataVersion + "\""); 159 } 160 } 161 162 // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797 163 @libcore.api.CorePlatformApi readFromFile(File file)164 public static TzDataSetVersion readFromFile(File file) throws IOException, TzDataSetException { 165 byte[] versionBytes = readBytes(file, TzDataSetVersion.TZ_DATA_VERSION_FILE_LENGTH); 166 return fromBytes(versionBytes); 167 } 168 169 /** Returns the major version number. See {@link TzDataSetVersion}. */ 170 @libcore.api.CorePlatformApi getFormatMajorVersion()171 public int getFormatMajorVersion() { 172 return formatMajorVersion; 173 } 174 175 /** Returns the minor version number. See {@link TzDataSetVersion}. */ 176 @libcore.api.CorePlatformApi getFormatMinorVersion()177 public int getFormatMinorVersion() { 178 return formatMinorVersion; 179 } 180 181 /** Returns the tzdb version string. See {@link TzDataSetVersion}. */ 182 @libcore.api.CorePlatformApi getRulesVersion()183 public String getRulesVersion() { 184 return rulesVersion; 185 } 186 187 // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797 188 /** Returns the Android revision. See {@link TzDataSetVersion}. */ 189 @libcore.api.CorePlatformApi getRevision()190 public int getRevision() { 191 return revision; 192 } 193 194 // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797 195 @libcore.api.CorePlatformApi toBytes()196 public byte[] toBytes() { 197 return toBytes(formatMajorVersion, formatMinorVersion, rulesVersion, revision); 198 } 199 toBytes( int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision)200 private static byte[] toBytes( 201 int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision) { 202 return (toFormatVersionString(majorFormatVersion, minorFormatVerison) 203 + "|" + rulesVersion + "|" + to3DigitVersionString(revision)) 204 .getBytes(StandardCharsets.US_ASCII); 205 } 206 207 @libcore.api.CorePlatformApi isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion)208 public static boolean isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion) { 209 return (CURRENT_FORMAT_MAJOR_VERSION == tzDataVersion.formatMajorVersion) 210 && (CURRENT_FORMAT_MINOR_VERSION <= tzDataVersion.formatMinorVersion); 211 } 212 213 @Override equals(Object o)214 public boolean equals(Object o) { 215 if (this == o) { 216 return true; 217 } 218 if (o == null || getClass() != o.getClass()) { 219 return false; 220 } 221 TzDataSetVersion that = (TzDataSetVersion) o; 222 if (formatMajorVersion != that.formatMajorVersion) { 223 return false; 224 } 225 if (formatMinorVersion != that.formatMinorVersion) { 226 return false; 227 } 228 if (revision != that.revision) { 229 return false; 230 } 231 return rulesVersion.equals(that.rulesVersion); 232 } 233 234 @Override hashCode()235 public int hashCode() { 236 int result = formatMajorVersion; 237 result = 31 * result + formatMinorVersion; 238 result = 31 * result + rulesVersion.hashCode(); 239 result = 31 * result + revision; 240 return result; 241 } 242 243 @Override toString()244 public String toString() { 245 return "TzDataSetVersion{" + 246 "formatMajorVersion=" + formatMajorVersion + 247 ", formatMinorVersion=" + formatMinorVersion + 248 ", rulesVersion='" + rulesVersion + '\'' + 249 ", revision=" + revision + 250 '}'; 251 } 252 253 /** 254 * Returns a version as a zero-padded three-digit String value. 255 */ to3DigitVersionString(int version)256 private static String to3DigitVersionString(int version) { 257 try { 258 return String.format(Locale.ROOT, "%03d", validate3DigitVersion(version)); 259 } catch (TzDataSetException e) { 260 throw new IllegalArgumentException(e); 261 } 262 } 263 264 /** 265 * Validates and parses a zero-padded three-digit String value. 266 */ from3DigitVersionString(String versionString)267 private static int from3DigitVersionString(String versionString) throws TzDataSetException { 268 final String parseErrorMessage = "versionString must be a zero padded, 3 digit, positive" 269 + " decimal integer"; 270 if (versionString.length() != 3) { 271 throw new TzDataSetException(parseErrorMessage); 272 } 273 try { 274 int version = Integer.parseInt(versionString); 275 return validate3DigitVersion(version); 276 } catch (NumberFormatException e) { 277 throw new TzDataSetException(parseErrorMessage, e); 278 } 279 } 280 validate3DigitVersion(int value)281 private static int validate3DigitVersion(int value) throws TzDataSetException { 282 // 0 is allowed but is reserved for testing. 283 if (value < 0 || value > 999) { 284 throw new TzDataSetException("Expected 0 <= value <= 999, was " + value); 285 } 286 return value; 287 } 288 289 // @VisibleForTesting toFormatVersionString(int majorFormatVersion, int minorFormatVersion)290 public 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