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