blob: 9610d459e8cc362b7ab847c4dde89525ff069ff9 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.timezone.distro;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Constants and logic associated with the time zone distro version file.
*/
public class DistroVersion {
/**
* The major distro format version supported by this device.
* Increment this for non-backwards compatible changes to the distro format. Reset the minor
* version to 1 when doing so.
* This constant must match the one in system/core/tzdatacheck/tzdatacheck.cpp.
*/
public static final int CURRENT_FORMAT_MAJOR_VERSION = 3; // Android Q
/**
* The minor distro format version supported by this device. Increment this for
* backwards-compatible changes to the distro format.
* This constant must match the one in system/core/tzdatacheck/tzdatacheck.cpp.
*/
public static final int CURRENT_FORMAT_MINOR_VERSION = 1;
/** The full major + minor distro format version for this device. */
private static final String FULL_CURRENT_FORMAT_VERSION_STRING =
toFormatVersionString(CURRENT_FORMAT_MAJOR_VERSION, CURRENT_FORMAT_MINOR_VERSION);
private static final int FORMAT_VERSION_STRING_LENGTH =
FULL_CURRENT_FORMAT_VERSION_STRING.length();
private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})");
/** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */
private static final Pattern RULES_VERSION_PATTERN = Pattern.compile("(\\d{4}\\w)");
private static final int RULES_VERSION_LENGTH = 5;
/** A pattern that matches the revision of a rules update. e.g. "001" */
private static final Pattern REVISION_PATTERN = Pattern.compile("(\\d{3})");
private static final int REVISION_LENGTH = 3;
/**
* The length of a well-formed distro version file:
* {Distro version}|{Rule version}|{Revision}
*/
public static final int DISTRO_VERSION_FILE_LENGTH = FORMAT_VERSION_STRING_LENGTH + 1
+ RULES_VERSION_LENGTH
+ 1 + REVISION_LENGTH;
private static final Pattern DISTRO_VERSION_PATTERN = Pattern.compile(
FORMAT_VERSION_PATTERN.pattern() + "\\|"
+ RULES_VERSION_PATTERN.pattern() + "\\|"
+ REVISION_PATTERN.pattern()
+ ".*" /* ignore trailing */);
public final int formatMajorVersion;
public final int formatMinorVersion;
public final String rulesVersion;
public final int revision;
public DistroVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion,
int revision) throws DistroException {
this.formatMajorVersion = validate3DigitVersion(formatMajorVersion);
this.formatMinorVersion = validate3DigitVersion(formatMinorVersion);
if (!RULES_VERSION_PATTERN.matcher(rulesVersion).matches()) {
throw new DistroException("Invalid rulesVersion: " + rulesVersion);
}
this.rulesVersion = rulesVersion;
this.revision = validate3DigitVersion(revision);
}
public static DistroVersion fromBytes(byte[] bytes) throws DistroException {
String distroVersion = new String(bytes, StandardCharsets.US_ASCII);
try {
Matcher matcher = DISTRO_VERSION_PATTERN.matcher(distroVersion);
if (!matcher.matches()) {
throw new DistroException(
"Invalid distro version string: \"" + distroVersion + "\"");
}
String formatMajorVersion = matcher.group(1);
String formatMinorVersion = matcher.group(2);
String rulesVersion = matcher.group(3);
String revision = matcher.group(4);
return new DistroVersion(
from3DigitVersionString(formatMajorVersion),
from3DigitVersionString(formatMinorVersion),
rulesVersion,
from3DigitVersionString(revision));
} catch (IndexOutOfBoundsException e) {
// The use of the regexp above should make this impossible.
throw new DistroException("Distro version string too short: \"" + distroVersion + "\"");
}
}
public byte[] toBytes() {
return toBytes(formatMajorVersion, formatMinorVersion, rulesVersion, revision);
}
// @VisibleForTesting - can be used to construct invalid distro version bytes.
public static byte[] toBytes(
int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision) {
return (toFormatVersionString(majorFormatVersion, minorFormatVerison)
+ "|" + rulesVersion + "|" + to3DigitVersionString(revision))
.getBytes(StandardCharsets.US_ASCII);
}
public static boolean isCompatibleWithThisDevice(DistroVersion distroVersion) {
return (CURRENT_FORMAT_MAJOR_VERSION == distroVersion.formatMajorVersion)
&& (CURRENT_FORMAT_MINOR_VERSION <= distroVersion.formatMinorVersion);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DistroVersion that = (DistroVersion) o;
if (formatMajorVersion != that.formatMajorVersion) {
return false;
}
if (formatMinorVersion != that.formatMinorVersion) {
return false;
}
if (revision != that.revision) {
return false;
}
return rulesVersion.equals(that.rulesVersion);
}
@Override
public int hashCode() {
int result = formatMajorVersion;
result = 31 * result + formatMinorVersion;
result = 31 * result + rulesVersion.hashCode();
result = 31 * result + revision;
return result;
}
@Override
public String toString() {
return "DistroVersion{" +
"formatMajorVersion=" + formatMajorVersion +
", formatMinorVersion=" + formatMinorVersion +
", rulesVersion='" + rulesVersion + '\'' +
", revision=" + revision +
'}';
}
/**
* Returns a version as a zero-padded three-digit String value.
*/
private static String to3DigitVersionString(int version) {
try {
return String.format(Locale.ROOT, "%03d", validate3DigitVersion(version));
} catch (DistroException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Validates and parses a zero-padded three-digit String value.
*/
private static int from3DigitVersionString(String versionString) throws DistroException {
final String parseErrorMessage = "versionString must be a zero padded, 3 digit, positive"
+ " decimal integer";
if (versionString.length() != 3) {
throw new DistroException(parseErrorMessage);
}
try {
int version = Integer.parseInt(versionString);
return validate3DigitVersion(version);
} catch (NumberFormatException e) {
throw new DistroException(parseErrorMessage, e);
}
}
private static int validate3DigitVersion(int value) throws DistroException {
// 0 is allowed but is reserved for testing.
if (value < 0 || value > 999) {
throw new DistroException("Expected 0 <= value <= 999, was " + value);
}
return value;
}
private static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) {
return to3DigitVersionString(majorFormatVersion)
+ "." + to3DigitVersionString(minorFormatVersion);
}
}