| /******************************************************************************* |
| * Copyright (c) 2000, 2017 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM - Initial API and implementation |
| * G&H Softwareentwicklung GmbH - internationalization implementation (bug 150933) |
| * Cloudsmith Inc. Refactored for more general use with VersionedId |
| ******************************************************************************/ |
| package org.eclipse.equinox.internal.p2.updatesite; |
| |
| import java.util.*; |
| import org.eclipse.equinox.p2.metadata.IVersionedId; |
| import org.eclipse.equinox.p2.metadata.Version; |
| |
| /** |
| * Refactored from org.eclipse.pde.internal.build.builder.BuildDirector |
| */ |
| public class VersionSuffixGenerator { |
| public static final String VERSION_QUALIFIER = "qualifier"; //$NON-NLS-1$ |
| |
| private static final int QUALIFIER_SUFFIX_VERSION = 1; |
| |
| // The 64 characters that are legal in a version qualifier, in lexicographical order. |
| public static final String BASE_64_ENCODING = "-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; //$NON-NLS-1$ |
| |
| public static String incrementQualifier(String qualifier) { |
| int idx = qualifier.length() - 1; |
| |
| for (; idx >= 0; idx--) { |
| //finding last non-'z' character |
| if (qualifier.charAt(idx) != 'z') |
| break; |
| } |
| |
| if (idx >= 0) { |
| // charAt(idx) is < 'z', so don't need to check bounds |
| int c = BASE_64_ENCODING.indexOf(qualifier.charAt(idx)) + 1; |
| String newQualifier = qualifier.substring(0, idx); |
| newQualifier += BASE_64_ENCODING.charAt(c); |
| return newQualifier; |
| } |
| |
| return null; |
| } |
| |
| private static void appendEncodedCharacter(StringBuffer buffer, int c) { |
| while (c > 62) { |
| buffer.append('z'); |
| c -= 63; |
| } |
| buffer.append(base64Character(c)); |
| } |
| |
| // Integer to character conversion in our base-64 encoding scheme. If the |
| // input is out of range, an illegal character will be returned. |
| // |
| private static char base64Character(int number) { |
| return (number < 0 || number > 63) ? ' ' : BASE_64_ENCODING.charAt(number); |
| } |
| |
| private static int charValue(char c) { |
| int index = BASE_64_ENCODING.indexOf(c); |
| // The "+ 1" is very intentional. For a blank (or anything else that |
| // is not a legal character), we want to return 0. For legal |
| // characters, we want to return one greater than their position, so |
| // that a blank is correctly distinguished from '-'. |
| return index + 1; |
| } |
| |
| private static int computeNameSum(String name) { |
| int sum = 0; |
| int top = name.length(); |
| int lshift = 20; |
| for (int idx = 0; idx < top; ++idx) { |
| int c = name.charAt(idx) & 0xffff; |
| if (c == '.' && lshift > 0) |
| lshift -= 4; |
| else |
| sum += c << lshift; |
| } |
| return sum; |
| } |
| |
| private static int getIntSegment(Version v, int segment) { |
| int segCount = v.getSegmentCount(); |
| if (segCount <= segment) |
| return 0; |
| Object seg = v.getSegment(segment); |
| return seg instanceof Integer ? ((Integer) seg).intValue() : 0; |
| } |
| |
| private static int getMajor(Version v) { |
| return getIntSegment(v, 0); |
| } |
| |
| private static int getMicro(Version v) { |
| return getIntSegment(v, 2); |
| } |
| |
| private static int getMinor(Version v) { |
| return getIntSegment(v, 1); |
| } |
| |
| private static String getQualifier(Version v) { |
| int segCount = v.getSegmentCount(); |
| if (segCount == 0) |
| return null; |
| Object seg = v.getSegment(segCount - 1); |
| return seg instanceof String ? (String) seg : null; |
| } |
| |
| // Encode a non-negative number as a variable length string, with the |
| // property that if X > Y then the encoding of X is lexicographically |
| // greater than the enocding of Y. This is accomplished by encoding the |
| // length of the string at the beginning of the string. The string is a |
| // series of base 64 (6-bit) characters. The first three bits of the first |
| // character indicate the number of additional characters in the string. |
| // The last three bits of the first character and all of the rest of the |
| // characters encode the actual value of the number. Examples: |
| // 0 --> 000 000 --> "-" |
| // 7 --> 000 111 --> "6" |
| // 8 --> 001 000 001000 --> "77" |
| // 63 --> 001 000 111111 --> "7z" |
| // 64 --> 001 001 000000 --> "8-" |
| // 511 --> 001 111 111111 --> "Dz" |
| // 512 --> 010 000 001000 000000 --> "E7-" |
| // 2^32 - 1 --> 101 011 111111 ... 111111 --> "fzzzzz" |
| // 2^45 - 1 --> 111 111 111111 ... 111111 --> "zzzzzzzz" |
| // (There are some wasted values in this encoding. For example, |
| // "7-" through "76" and "E--" through "E6z" are not legal encodings of |
| // any number. But the benefit of filling in those wasted ranges would not |
| // be worth the added complexity.) |
| private static String lengthPrefixBase64(long number) { |
| int length = 7; |
| for (int i = 0; i < 7; ++i) { |
| if (number < (1L << ((i * 6) + 3))) { |
| length = i; |
| break; |
| } |
| } |
| StringBuilder result = new StringBuilder(length + 1); |
| result.append(base64Character((length << 3) + (int) ((number >> (6 * length)) & 0x7))); |
| while (--length >= 0) { |
| result.append(base64Character((int) ((number >> (6 * length)) & 0x3f))); |
| } |
| return result.toString(); |
| } |
| |
| private final int maxVersionSuffixLength; |
| |
| private final int significantDigits; |
| |
| public VersionSuffixGenerator() { |
| this(-1, -1); |
| } |
| |
| public VersionSuffixGenerator(int maxVersionSuffixLenght, int significantDigits) { |
| this.maxVersionSuffixLength = maxVersionSuffixLenght < 0 ? 45 : maxVersionSuffixLenght; |
| this.significantDigits = significantDigits < 0 ? Integer.MAX_VALUE : significantDigits; |
| } |
| |
| /** |
| * Version suffix generation. |
| * @param features A collection of @{link IVersionedId} instances representing the features to include |
| * @param others A list of @{link IVersionedId} instances representing other IUs to include |
| * @return The generated suffix or <code>null</code> |
| */ |
| public String generateSuffix(Collection<? extends IVersionedId> features, Collection<? extends IVersionedId> others) { |
| if (maxVersionSuffixLength <= 0 || (features.isEmpty() && others.isEmpty())) |
| return null; // do nothing |
| |
| long majorSum = 0L; |
| long minorSum = 0L; |
| long serviceSum = 0L; |
| long nameCharsSum = 0L; |
| |
| // Include the version of this algorithm as part of the suffix, so that |
| // we have a way to make sure all suffixes increase when the algorithm |
| // changes. |
| // |
| majorSum += QUALIFIER_SUFFIX_VERSION; |
| ArrayList<String> qualifiers = new ArrayList<>(); |
| |
| // Loop through the included features, adding the version number parts |
| // to the running totals and storing the qualifier suffixes. |
| // |
| Iterator<? extends IVersionedId> itor = features.iterator(); |
| while (itor.hasNext()) { |
| IVersionedId refFeature = itor.next(); |
| Version version = refFeature.getVersion(); |
| majorSum += getMajor(version); |
| minorSum += getMinor(version); |
| serviceSum += getMicro(version); |
| qualifiers.add(getQualifier(version)); |
| nameCharsSum = computeNameSum(refFeature.getId()); |
| } |
| |
| // Loop through the included plug-ins and fragments, adding the version |
| // number parts to the running totals and storing the qualifiers. |
| // |
| itor = features.iterator(); |
| while (itor.hasNext()) { |
| IVersionedId refOther = itor.next(); |
| Version version = refOther.getVersion(); |
| majorSum += getMajor(version); |
| minorSum += getMinor(version); |
| serviceSum += getMicro(version); |
| |
| String qualifier = getQualifier(version); |
| if (qualifier != null && qualifier.endsWith(VERSION_QUALIFIER)) { |
| int resultingLength = qualifier.length() - VERSION_QUALIFIER.length(); |
| if (resultingLength > 0) { |
| if (qualifier.charAt(resultingLength - 1) == '.') |
| resultingLength--; |
| qualifier = resultingLength > 0 ? qualifier.substring(0, resultingLength) : null; |
| } else |
| qualifier = null; |
| } |
| qualifiers.add(qualifier); |
| } |
| |
| // Limit the qualifiers to the specified number of significant digits, |
| // and figure out what the longest qualifier is. |
| // |
| int longestQualifier = 0; |
| int idx = qualifiers.size(); |
| while (--idx >= 0) { |
| String qualifier = qualifiers.get(idx); |
| if (qualifier == null) |
| continue; |
| |
| if (qualifier.length() > significantDigits) { |
| qualifier = qualifier.substring(0, significantDigits); |
| qualifiers.set(idx, qualifier); |
| } |
| if (qualifier.length() > longestQualifier) |
| longestQualifier = qualifier.length(); |
| } |
| |
| StringBuffer result = new StringBuffer(); |
| |
| // Encode the sums of the first three parts of the version numbers. |
| result.append(lengthPrefixBase64(majorSum)); |
| result.append(lengthPrefixBase64(minorSum)); |
| result.append(lengthPrefixBase64(serviceSum)); |
| result.append(lengthPrefixBase64(nameCharsSum)); |
| |
| if (longestQualifier > 0) { |
| // Calculate the sum at each position of the qualifiers. |
| int[] qualifierSums = new int[longestQualifier]; |
| int top = qualifiers.size(); |
| for (idx = 0; idx < top; ++idx) { |
| String qualifier = qualifiers.get(idx); |
| if (qualifier == null) |
| continue; |
| |
| int qlen = qualifier.length(); |
| for (int j = 0; j < qlen; ++j) |
| qualifierSums[j] += charValue(qualifier.charAt(j)); |
| } |
| |
| // Normalize the sums to be base 65. |
| int carry = 0; |
| for (int k = longestQualifier - 1; k >= 1; --k) { |
| qualifierSums[k] += carry; |
| carry = qualifierSums[k] / 65; |
| qualifierSums[k] = qualifierSums[k] % 65; |
| } |
| qualifierSums[0] += carry; |
| |
| // Always use one character for overflow. This will be handled |
| // correctly even when the overflow character itself overflows. |
| result.append(lengthPrefixBase64(qualifierSums[0])); |
| for (int m = 1; m < longestQualifier; ++m) |
| appendEncodedCharacter(result, qualifierSums[m]); |
| } |
| |
| // If the resulting suffix is too long, shorten it to the designed length. |
| // |
| if (result.length() > maxVersionSuffixLength) |
| result.setLength(maxVersionSuffixLength); |
| |
| // It is safe to strip any '-' characters from the end of the suffix. |
| // (This won't happen very often, but it will save us a character or |
| // two when it does.) |
| // |
| int len = result.length(); |
| while (len > 0 && result.charAt(len - 1) == '-') |
| result.setLength(--len); |
| return result.toString(); |
| } |
| } |