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