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