1 /******************************************************************************* 2 * Copyright (c) 2009, 2017 Cloudsmith Inc. and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * Cloudsmith Inc. - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.equinox.internal.p2.metadata; 15 16 import java.util.*; 17 import org.eclipse.equinox.p2.metadata.Version; 18 import org.eclipse.equinox.p2.metadata.VersionFormatException; 19 import org.eclipse.osgi.util.NLS; 20 21 /** 22 * The Omni Version parser. Not intended for public API. Instead use 23 * {@link Version#create(String)} or {@link Version#parseVersion(String)}. 24 * 25 * The class also contains some general purpose parser support methods 26 * 27 * @noextend This class is not intended to be subclassed by clients. 28 */ 29 public abstract class VersionParser { 30 public static final Integer MAX_INT_OBJ = Integer.MAX_VALUE; 31 valueOf(int i)32 public static Integer valueOf(int i) { 33 return (i == Integer.MAX_VALUE) ? MAX_INT_OBJ : Integer.valueOf(i); 34 } 35 removeRedundantTrail(List<Comparable<?>> segments, Comparable<?> padValue)36 static Comparable<?> removeRedundantTrail(List<Comparable<?>> segments, Comparable<?> padValue) { 37 Comparable<?> redundantTrail; 38 if (padValue == null) 39 redundantTrail = VersionVector.MIN_VALUE; 40 else { 41 redundantTrail = padValue; 42 if (padValue == VersionVector.MIN_VALUE) 43 padValue = null; 44 } 45 46 int idx = segments.size(); 47 while (--idx >= 0 && segments.get(idx).equals(redundantTrail)) 48 segments.remove(idx); 49 50 return padValue; 51 } 52 VersionParser()53 private VersionParser() { 54 // Prevent class from being instantiated 55 } 56 57 /** 58 * Parse the <code>version</code> string and assing the parsed portions to the <code>receiver</code>. 59 * This method is called from the version string constructor. 60 * 61 * @param version The string to be parsed 62 * @param start Start position in the <code>version</code> string 63 * @param maxPos End position in the <code>version</code> string 64 * @returns a version if one indeed was parsed or <code>null</code> if the string 65 * contained only whitespace. 66 * @throws IllegalArgumentException if the version is malformed 67 */ parse(String version, int start, int maxPos)68 public static Version parse(String version, int start, int maxPos) throws IllegalArgumentException { 69 // trim leading and trailing whitespace 70 int pos = skipWhite(version, start); 71 maxPos = skipTrailingWhite(version, start, maxPos); 72 if (pos == maxPos) 73 return null; 74 75 List<Comparable<?>> vector = null; 76 VersionFormat fmt = null; 77 char c = version.charAt(pos); 78 if (isDigit(c)) { 79 return OSGiVersion.fromVector(VersionFormat.OSGI_FORMAT.parse(version, pos, maxPos)); 80 } 81 82 if (!isLetter(c)) 83 throw new IllegalArgumentException(); 84 85 if (version.startsWith(Version.RAW_PREFIX, pos)) { 86 VersionFormat rawFmt = VersionFormat.RAW_FORMAT; 87 pos += 4; 88 89 // Find ending '/' that is neither quoted or escaped 90 int end = maxPos; 91 for (int idx = pos; idx < maxPos; ++idx) { 92 c = version.charAt(idx); 93 switch (c) { 94 case '/' : 95 end = idx; 96 break; 97 case '\\' : 98 ++idx; 99 continue; 100 case '\'' : 101 case '"' : 102 for (++idx; idx < maxPos; ++idx) { 103 char e = version.charAt(idx); 104 if (e == c) { 105 break; 106 } 107 if (e == '\\') 108 ++idx; 109 } 110 // fall through to default 111 default : 112 continue; 113 } 114 break; 115 } 116 117 vector = rawFmt.parse(version, pos, end); 118 pos = end; 119 if (pos == maxPos) 120 // This was a pure raw version 121 // 122 return OmniVersion.fromVector(vector, null, null); 123 124 if (version.charAt(pos) != '/') 125 throw new IllegalArgumentException(NLS.bind(Messages.expected_slash_after_raw_vector_0, version.substring(start, maxPos))); 126 ++pos; 127 128 if (pos == maxPos) 129 throw new IllegalArgumentException(NLS.bind(Messages.expected_orignal_after_slash_0, version.substring(start, maxPos))); 130 } 131 132 if (version.startsWith("format(", pos)) { //$NON-NLS-1$ 133 // Parse the format 134 // 135 pos += 7; 136 try { 137 // Find matching ')' that is neither quoted or escaped 138 // 139 int end = findEndOfFormat(version, pos, maxPos); 140 fmt = VersionFormat.compile(version, pos, end); 141 pos = end + 1; 142 } catch (VersionFormatException e) { 143 throw new IllegalArgumentException(e.getMessage(), e); 144 } 145 if (pos == maxPos) { 146 // This was a raw version with format but no original 147 // 148 if (vector == null) 149 throw new IllegalArgumentException(NLS.bind(Messages.only_format_specified_0, version.substring(start, maxPos))); 150 return fmt == VersionFormat.OSGI_FORMAT ? OSGiVersion.fromVector(vector) : OmniVersion.fromVector(vector, fmt, null); 151 } 152 } 153 154 if (fmt == null && vector == null) 155 throw new IllegalArgumentException(NLS.bind(Messages.neither_raw_vector_nor_format_specified_0, version.substring(start, maxPos))); 156 157 if (version.charAt(pos) != ':') 158 throw new IllegalArgumentException(NLS.bind(Messages.colon_expected_before_original_version_0, version.substring(start, maxPos))); 159 160 pos++; 161 if (pos == maxPos) 162 throw new IllegalArgumentException(NLS.bind(Messages.expected_orignal_after_colon_0, version.substring(start, maxPos))); 163 164 if (vector == null) { 165 // Vector and pad must be created by parsing the original 166 // 167 vector = fmt.parse(version, pos, maxPos); 168 } 169 return fmt == VersionFormat.OSGI_FORMAT ? OSGiVersion.fromVector(vector) : OmniVersion.fromVector(vector, fmt, version.substring(pos)); 170 } 171 isDigit(char c)172 static boolean isDigit(char c) { 173 return c >= '0' && c <= '9'; 174 } 175 isLetter(char c)176 public static boolean isLetter(char c) { 177 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); 178 } 179 isLetterOrDigit(char c)180 static boolean isLetterOrDigit(char c) { 181 return isDigit(c) || isLetter(c); 182 } 183 findEndOfFormat(String string, int pos, int maxPos)184 public static int findEndOfFormat(String string, int pos, int maxPos) { 185 int end = -1; 186 int depth = 1; 187 for (int idx = pos; idx < maxPos; ++idx) { 188 char c = string.charAt(idx); 189 switch (c) { 190 case ')' : 191 if (--depth == 0) { 192 end = idx; 193 break; 194 } 195 continue; 196 case '(' : 197 ++depth; 198 continue; 199 case '\\' : 200 ++idx; 201 continue; 202 case '\'' : 203 case '"' : 204 for (++idx; idx < maxPos; ++idx) { 205 char e = string.charAt(idx); 206 if (e == c) { 207 break; 208 } 209 if (e == '\\') 210 ++idx; 211 } 212 // fall through to default 213 default : 214 continue; 215 } 216 break; 217 } 218 if (depth != 0) 219 throw new IllegalArgumentException(NLS.bind(Messages.unbalanced_format_parenthesis, string.substring(pos - 1, maxPos))); 220 return end; 221 } 222 parseRawElement(String value, int[] position, int maxPos)223 static Comparable<?> parseRawElement(String value, int[] position, int maxPos) { 224 int current = position[0]; 225 if (current >= maxPos) 226 return null; 227 228 boolean negate = false; 229 char c = value.charAt(current); 230 Comparable<?> v; 231 switch (c) { 232 case '\'' : 233 case '"' : { 234 StringBuilder sb = new StringBuilder(); 235 for (;;) { 236 char q = c; 237 if (++current == maxPos) 238 return null; 239 c = value.charAt(current); 240 while (c != q) { 241 if (c < 32) 242 return null; 243 sb.append(c); 244 if (++current == maxPos) 245 return null; 246 c = value.charAt(current); 247 } 248 if (++current == maxPos) 249 break; 250 c = value.charAt(current); 251 if (c != '\'' && c != '"') 252 break; 253 } 254 v = sb.length() == 0 ? VersionVector.MINS_VALUE : sb.toString(); 255 break; 256 } 257 case '{' : { 258 if (++current == maxPos) 259 return null; 260 261 position[0] = current; 262 v = parseRawEnum(value, position, maxPos); 263 if (v == null) 264 return null; 265 current = position[0]; 266 break; 267 } 268 case '<' : { 269 if (++current == maxPos) 270 return null; 271 272 position[0] = current; 273 v = parseRawVector(value, position, maxPos); 274 if (v == null) 275 return null; 276 current = position[0]; 277 break; 278 } 279 case 'm' : 280 v = VersionVector.MAXS_VALUE; 281 ++current; 282 break; 283 case 'M' : 284 v = VersionVector.MAX_VALUE; 285 ++current; 286 break; 287 case '-' : 288 if (++current >= maxPos) 289 return null; 290 291 c = value.charAt(current); 292 if (c == 'M') { 293 ++current; 294 v = VersionVector.MIN_VALUE; 295 break; 296 } 297 negate = true; 298 // Fall through to default 299 default : { 300 if (isDigit(c)) { 301 int start = current++; 302 while (current < maxPos && isDigit(value.charAt(current))) 303 ++current; 304 int val = Integer.parseInt(value.substring(start, current)); 305 if (negate) 306 val = -val; 307 v = valueOf(val); 308 break; 309 } 310 return null; 311 } 312 } 313 position[0] = current; 314 return v; 315 } 316 parseRawVector(String value, int[] position, int maxPos)317 private static Comparable<?> parseRawVector(String value, int[] position, int maxPos) { 318 int pos = position[0]; 319 if (pos >= maxPos) 320 return null; 321 322 char c = value.charAt(pos); 323 if (c == '>') 324 return null; 325 326 ArrayList<Comparable<?>> rawList = new ArrayList<>(); 327 boolean padMarkerSeen = (c == 'p'); 328 if (padMarkerSeen) { 329 if (++pos >= maxPos) 330 return null; 331 position[0] = pos; 332 } 333 334 Comparable<?> pad = null; 335 for (;;) { 336 Comparable<?> elem = parseRawElement(value, position, maxPos); 337 if (elem == null) 338 return null; 339 340 if (padMarkerSeen) 341 pad = elem; 342 else 343 rawList.add(elem); 344 345 pos = position[0]; 346 if (pos >= maxPos) 347 return null; 348 349 c = value.charAt(pos); 350 position[0] = ++pos; 351 if (c == '>') 352 break; 353 354 if (padMarkerSeen || pos >= maxPos) 355 return null; 356 357 if (c == 'p') { 358 padMarkerSeen = true; 359 continue; 360 } 361 362 if (c != '.') 363 return null; 364 } 365 pad = removeRedundantTrail(rawList, pad); 366 return new VersionVector(rawList.toArray(new Comparable[rawList.size()]), pad); 367 } 368 parseRawEnum(String value, int[] position, int maxPos)369 private static Comparable<?> parseRawEnum(String value, int[] position, int maxPos) { 370 int pos = position[0]; 371 ArrayList<List<String>> identifiers = new ArrayList<>(); 372 int ordinal = -1; 373 StringBuilder sb = new StringBuilder(); 374 for (;;) { 375 if (pos >= maxPos) 376 return null; 377 378 char c = value.charAt(pos++); 379 while (c != '}' && c != ',') { 380 if (pos == maxPos) 381 return null; 382 if (c <= ' ') 383 return null; 384 if (c == '\\' || c == '^') { 385 if (c == '^') 386 ordinal = identifiers.size(); 387 c = value.charAt(pos++); 388 if (pos == maxPos) 389 return null; 390 } 391 sb.append(c); 392 c = value.charAt(pos++); 393 } 394 identifiers.add(Collections.singletonList(sb.toString())); 395 if (c == '}') 396 break; 397 // c must be ',' at this point 398 sb.setLength(0); 399 } 400 if (ordinal == -1) 401 return null; 402 position[0] = pos; 403 return EnumDefinition.getSegment(identifiers, ordinal); 404 } 405 skipWhite(String string, int pos)406 public static int skipWhite(String string, int pos) { 407 int top = string.length(); 408 while (pos < top && string.charAt(pos) <= ' ') 409 ++pos; 410 return pos; 411 } 412 skipTrailingWhite(String string, int start, int end)413 public static int skipTrailingWhite(String string, int start, int end) { 414 while (end > start && string.charAt(end - 1) <= ' ') 415 --end; 416 return end; 417 } 418 } 419