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