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