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 17package com.android.timezone.distro; 18 19import java.nio.charset.StandardCharsets; 20import java.util.Locale; 21import java.util.regex.Matcher; 22import java.util.regex.Pattern; 23 24/** 25 * Constants and logic associated with the time zone distro version file. 26 */ 27public class DistroVersion { 28 29 /** 30 * The major distro format version supported by this device. 31 * Increment this for non-backwards compatible changes to the distro format. Reset the minor 32 * version to 1 when doing so. 33 * This constant must match the one in system/core/tzdatacheck/tzdatacheck.cpp. 34 */ 35 public static final int CURRENT_FORMAT_MAJOR_VERSION = 1; 36 37 /** 38 * The minor distro format version supported by this device. Increment this for 39 * backwards-compatible changes to the distro format. 40 * This constant must match the one in system/core/tzdatacheck/tzdatacheck.cpp. 41 */ 42 public static final int CURRENT_FORMAT_MINOR_VERSION = 1; 43 44 /** The full major + minor distro format version for this device. */ 45 private static final String FULL_CURRENT_FORMAT_VERSION_STRING = 46 toFormatVersionString(CURRENT_FORMAT_MAJOR_VERSION, CURRENT_FORMAT_MINOR_VERSION); 47 48 private static final int FORMAT_VERSION_STRING_LENGTH = 49 FULL_CURRENT_FORMAT_VERSION_STRING.length(); 50 private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})"); 51 52 /** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */ 53 private static final Pattern RULES_VERSION_PATTERN = Pattern.compile("(\\d{4}\\w)"); 54 55 private static final int RULES_VERSION_LENGTH = 5; 56 57 /** A pattern that matches the revision of a rules update. e.g. "001" */ 58 private static final Pattern REVISION_PATTERN = Pattern.compile("(\\d{3})"); 59 60 private static final int REVISION_LENGTH = 3; 61 62 /** 63 * The length of a well-formed distro version file: 64 * {Distro version}|{Rule version}|{Revision} 65 */ 66 public static final int DISTRO_VERSION_FILE_LENGTH = FORMAT_VERSION_STRING_LENGTH + 1 67 + RULES_VERSION_LENGTH 68 + 1 + REVISION_LENGTH; 69 70 private static final Pattern DISTRO_VERSION_PATTERN = Pattern.compile( 71 FORMAT_VERSION_PATTERN.pattern() + "\\|" 72 + RULES_VERSION_PATTERN.pattern() + "\\|" 73 + REVISION_PATTERN.pattern() 74 + ".*" /* ignore trailing */); 75 76 public final int formatMajorVersion; 77 public final int formatMinorVersion; 78 public final String rulesVersion; 79 public final int revision; 80 81 public DistroVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion, 82 int revision) throws DistroException { 83 this.formatMajorVersion = validate3DigitVersion(formatMajorVersion); 84 this.formatMinorVersion = validate3DigitVersion(formatMinorVersion); 85 if (!RULES_VERSION_PATTERN.matcher(rulesVersion).matches()) { 86 throw new DistroException("Invalid rulesVersion: " + rulesVersion); 87 } 88 this.rulesVersion = rulesVersion; 89 this.revision = validate3DigitVersion(revision); 90 } 91 92 public static DistroVersion fromBytes(byte[] bytes) throws DistroException { 93 String distroVersion = new String(bytes, StandardCharsets.US_ASCII); 94 try { 95 Matcher matcher = DISTRO_VERSION_PATTERN.matcher(distroVersion); 96 if (!matcher.matches()) { 97 throw new DistroException( 98 "Invalid distro version string: \"" + distroVersion + "\""); 99 } 100 String formatMajorVersion = matcher.group(1); 101 String formatMinorVersion = matcher.group(2); 102 String rulesVersion = matcher.group(3); 103 String revision = matcher.group(4); 104 return new DistroVersion( 105 from3DigitVersionString(formatMajorVersion), 106 from3DigitVersionString(formatMinorVersion), 107 rulesVersion, 108 from3DigitVersionString(revision)); 109 } catch (IndexOutOfBoundsException e) { 110 // The use of the regexp above should make this impossible. 111 throw new DistroException("Distro version string too short: \"" + distroVersion + "\""); 112 } 113 } 114 115 public byte[] toBytes() { 116 return toBytes(formatMajorVersion, formatMinorVersion, rulesVersion, revision); 117 } 118 119 // @VisibleForTesting - can be used to construct invalid distro version bytes. 120 public static byte[] toBytes( 121 int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision) { 122 return (toFormatVersionString(majorFormatVersion, minorFormatVerison) 123 + "|" + rulesVersion + "|" + to3DigitVersionString(revision)) 124 .getBytes(StandardCharsets.US_ASCII); 125 } 126 127 public static boolean isCompatibleWithThisDevice(DistroVersion distroVersion) { 128 return (CURRENT_FORMAT_MAJOR_VERSION == distroVersion.formatMajorVersion) 129 && (CURRENT_FORMAT_MINOR_VERSION <= distroVersion.formatMinorVersion); 130 } 131 132 @Override 133 public boolean equals(Object o) { 134 if (this == o) { 135 return true; 136 } 137 if (o == null || getClass() != o.getClass()) { 138 return false; 139 } 140 141 DistroVersion that = (DistroVersion) o; 142 143 if (formatMajorVersion != that.formatMajorVersion) { 144 return false; 145 } 146 if (formatMinorVersion != that.formatMinorVersion) { 147 return false; 148 } 149 if (revision != that.revision) { 150 return false; 151 } 152 return rulesVersion.equals(that.rulesVersion); 153 } 154 155 @Override 156 public int hashCode() { 157 int result = formatMajorVersion; 158 result = 31 * result + formatMinorVersion; 159 result = 31 * result + rulesVersion.hashCode(); 160 result = 31 * result + revision; 161 return result; 162 } 163 164 @Override 165 public String toString() { 166 return "DistroVersion{" + 167 "formatMajorVersion=" + formatMajorVersion + 168 ", formatMinorVersion=" + formatMinorVersion + 169 ", rulesVersion='" + rulesVersion + '\'' + 170 ", revision=" + revision + 171 '}'; 172 } 173 174 /** 175 * Returns a version as a zero-padded three-digit String value. 176 */ 177 private static String to3DigitVersionString(int version) { 178 try { 179 return String.format(Locale.ROOT, "%03d", validate3DigitVersion(version)); 180 } catch (DistroException e) { 181 throw new IllegalArgumentException(e); 182 } 183 } 184 185 /** 186 * Validates and parses a zero-padded three-digit String value. 187 */ 188 private static int from3DigitVersionString(String versionString) throws DistroException { 189 final String parseErrorMessage = "versionString must be a zero padded, 3 digit, positive" 190 + " decimal integer"; 191 if (versionString.length() != 3) { 192 throw new DistroException(parseErrorMessage); 193 } 194 try { 195 int version = Integer.parseInt(versionString); 196 return validate3DigitVersion(version); 197 } catch (NumberFormatException e) { 198 throw new DistroException(parseErrorMessage, e); 199 } 200 } 201 202 private static int validate3DigitVersion(int value) throws DistroException { 203 // 0 is allowed but is reserved for testing. 204 if (value < 0 || value > 999) { 205 throw new DistroException("Expected 0 <= value <= 999, was " + value); 206 } 207 return value; 208 } 209 210 private static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) { 211 return to3DigitVersionString(majorFormatVersion) 212 + "." + to3DigitVersionString(minorFormatVersion); 213 } 214} 215