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