/******************************************************************************* * Copyright (c) 2009, 2016 Cloudsmith Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Cloudsmith Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.equinox.internal.p2.metadata; import java.util.*; import org.eclipse.equinox.p2.metadata.Version; import org.eclipse.equinox.p2.metadata.VersionFormatException; import org.eclipse.osgi.util.NLS; /** * The Omni Version parser. Not intended for public API. Instead use * {@link Version#create(String)} or {@link Version#parseVersion(String)}. * * The class also contains some general purpose parser support methods * * @noextend This class is not intended to be subclassed by clients. */ public abstract class VersionParser { public static final Integer MAX_INT_OBJ = Integer.MAX_VALUE; public static Integer valueOf(int i) { return (i == Integer.MAX_VALUE) ? MAX_INT_OBJ : Integer.valueOf(i); } static Comparable<?> removeRedundantTrail(List<Comparable<?>> segments, Comparable<?> padValue) { Comparable<?> redundantTrail; if (padValue == null) redundantTrail = VersionVector.MIN_VALUE; else { redundantTrail = padValue; if (padValue == VersionVector.MIN_VALUE) padValue = null; } int idx = segments.size(); while (--idx >= 0 && segments.get(idx).equals(redundantTrail)) segments.remove(idx); return padValue; } private VersionParser() { // Prevent class from being instantiated } /** * Parse the <code>version</code> string and assing the parsed portions to the <code>receiver</code>. * This method is called from the version string constructor. * * @param version The string to be parsed * @param start Start position in the <code>version</code> string * @param maxPos End position in the <code>version</code> string * @returns a version if one indeed was parsed or <code>null</code> if the string * contained only whitespace. * @throws IllegalArgumentException if the version is malformed */ public static Version parse(String version, int start, int maxPos) throws IllegalArgumentException { // trim leading and trailing whitespace int pos = skipWhite(version, start); maxPos = skipTrailingWhite(version, start, maxPos); if (pos == maxPos) return null; List<Comparable<?>> vector = null; VersionFormat fmt = null; char c = version.charAt(pos); if (isDigit(c)) { return OSGiVersion.fromVector(VersionFormat.OSGI_FORMAT.parse(version, pos, maxPos)); } if (!isLetter(c)) throw new IllegalArgumentException(); if (version.startsWith(Version.RAW_PREFIX, pos)) { VersionFormat rawFmt = VersionFormat.RAW_FORMAT; pos += 4; // Find ending '/' that is neither quoted or escaped int end = maxPos; for (int idx = pos; idx < maxPos; ++idx) { c = version.charAt(idx); switch (c) { case '/' : end = idx; break; case '\\' : ++idx; continue; case '\'' : case '"' : for (++idx; idx < maxPos; ++idx) { char e = version.charAt(idx); if (e == c) { break; } if (e == '\\') ++idx; } // fall through to default default : continue; } break; } vector = rawFmt.parse(version, pos, end); pos = end; if (pos == maxPos) // This was a pure raw version // return OmniVersion.fromVector(vector, null, null); if (version.charAt(pos) != '/') throw new IllegalArgumentException(NLS.bind(Messages.expected_slash_after_raw_vector_0, version.substring(start, maxPos))); ++pos; if (pos == maxPos) throw new IllegalArgumentException(NLS.bind(Messages.expected_orignal_after_slash_0, version.substring(start, maxPos))); } if (version.startsWith("format(", pos)) { //$NON-NLS-1$ // Parse the format // pos += 7; try { // Find matching ')' that is neither quoted or escaped // int end = findEndOfFormat(version, pos, maxPos); fmt = VersionFormat.compile(version, pos, end); pos = end + 1; } catch (VersionFormatException e) { throw new IllegalArgumentException(e.getMessage(), e); } if (pos == maxPos) { // This was a raw version with format but no original // if (vector == null) throw new IllegalArgumentException(NLS.bind(Messages.only_format_specified_0, version.substring(start, maxPos))); return fmt == VersionFormat.OSGI_FORMAT ? OSGiVersion.fromVector(vector) : OmniVersion.fromVector(vector, fmt, null); } } if (fmt == null && vector == null) throw new IllegalArgumentException(NLS.bind(Messages.neither_raw_vector_nor_format_specified_0, version.substring(start, maxPos))); if (version.charAt(pos) != ':') throw new IllegalArgumentException(NLS.bind(Messages.colon_expected_before_original_version_0, version.substring(start, maxPos))); pos++; if (pos == maxPos) throw new IllegalArgumentException(NLS.bind(Messages.expected_orignal_after_colon_0, version.substring(start, maxPos))); if (vector == null) { // Vector and pad must be created by parsing the original // vector = fmt.parse(version, pos, maxPos); } return fmt == VersionFormat.OSGI_FORMAT ? OSGiVersion.fromVector(vector) : OmniVersion.fromVector(vector, fmt, version.substring(pos)); } static boolean isDigit(char c) { return c >= '0' && c <= '9'; } public static boolean isLetter(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } static boolean isLetterOrDigit(char c) { return isDigit(c) || isLetter(c); } public static int findEndOfFormat(String string, int pos, int maxPos) { int end = -1; int depth = 1; for (int idx = pos; idx < maxPos; ++idx) { char c = string.charAt(idx); switch (c) { case ')' : if (--depth == 0) { end = idx; break; } continue; case '(' : ++depth; continue; case '\\' : ++idx; continue; case '\'' : case '"' : for (++idx; idx < maxPos; ++idx) { char e = string.charAt(idx); if (e == c) { break; } if (e == '\\') ++idx; } // fall through to default default : continue; } break; } if (depth != 0) throw new IllegalArgumentException(NLS.bind(Messages.unbalanced_format_parenthesis, string.substring(pos - 1, maxPos))); return end; } static Comparable<?> parseRawElement(String value, int[] position, int maxPos) { int current = position[0]; if (current >= maxPos) return null; boolean negate = false; char c = value.charAt(current); Comparable<?> v; switch (c) { case '\'' : case '"' : { StringBuffer sb = new StringBuffer(); for (;;) { char q = c; if (++current == maxPos) return null; c = value.charAt(current); while (c != q) { if (c < 32) return null; sb.append(c); if (++current == maxPos) return null; c = value.charAt(current); } if (++current == maxPos) break; c = value.charAt(current); if (c != '\'' && c != '"') break; } v = sb.length() == 0 ? VersionVector.MINS_VALUE : sb.toString(); break; } case '{' : { if (++current == maxPos) return null; position[0] = current; v = parseRawEnum(value, position, maxPos); if (v == null) return null; current = position[0]; break; } case '<' : { if (++current == maxPos) return null; position[0] = current; v = parseRawVector(value, position, maxPos); if (v == null) return null; current = position[0]; break; } case 'm' : v = VersionVector.MAXS_VALUE; ++current; break; case 'M' : v = VersionVector.MAX_VALUE; ++current; break; case '-' : if (++current >= maxPos) return null; c = value.charAt(current); if (c == 'M') { ++current; v = VersionVector.MIN_VALUE; break; } negate = true; // Fall through to default default : { if (isDigit(c)) { int start = current++; while (current < maxPos && isDigit(value.charAt(current))) ++current; int val = Integer.parseInt(value.substring(start, current)); if (negate) val = -val; v = valueOf(val); break; } return null; } } position[0] = current; return v; } private static Comparable<?> parseRawVector(String value, int[] position, int maxPos) { int pos = position[0]; if (pos >= maxPos) return null; char c = value.charAt(pos); if (c == '>') return null; ArrayList<Comparable<?>> rawList = new ArrayList<Comparable<?>>(); boolean padMarkerSeen = (c == 'p'); if (padMarkerSeen) { if (++pos >= maxPos) return null; position[0] = pos; } Comparable<?> pad = null; for (;;) { Comparable<?> elem = parseRawElement(value, position, maxPos); if (elem == null) return null; if (padMarkerSeen) pad = elem; else rawList.add(elem); pos = position[0]; if (pos >= maxPos) return null; c = value.charAt(pos); position[0] = ++pos; if (c == '>') break; if (padMarkerSeen || pos >= maxPos) return null; if (c == 'p') { padMarkerSeen = true; continue; } if (c != '.') return null; } pad = removeRedundantTrail(rawList, pad); return new VersionVector(rawList.toArray(new Comparable[rawList.size()]), pad); } private static Comparable<?> parseRawEnum(String value, int[] position, int maxPos) { int pos = position[0]; ArrayList<List<String>> identifiers = new ArrayList<List<String>>(); int ordinal = -1; StringBuffer sb = new StringBuffer(); for (;;) { if (pos >= maxPos) return null; char c = value.charAt(pos++); while (c != '}' && c != ',') { if (pos == maxPos) return null; if (c <= ' ') return null; if (c == '\\' || c == '^') { if (c == '^') ordinal = identifiers.size(); c = value.charAt(pos++); if (pos == maxPos) return null; } sb.append(c); c = value.charAt(pos++); } identifiers.add(Collections.singletonList(sb.toString())); if (c == '}') break; // c must be ',' at this point sb.setLength(0); } if (ordinal == -1) return null; position[0] = pos; return EnumDefinition.getSegment(identifiers, ordinal); } public static int skipWhite(String string, int pos) { int top = string.length(); while (pos < top && string.charAt(pos) <= ' ') ++pos; return pos; } public static int skipTrailingWhite(String string, int start, int end) { while (end > start && string.charAt(end - 1) <= ' ') --end; return end; } }