Skip to main content
summaryrefslogtreecommitdiffstats
blob: 15d483343d057fd1344419956daba90b4df7a10e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/*******************************************************************************
 * Copyright (c) 2009, 2012 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.p2.internal.repository.comparator;

import java.io.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.jar.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.publisher.eclipse.FeatureParser;
import org.eclipse.equinox.p2.internal.repository.comparator.java.*;
import org.eclipse.equinox.p2.publisher.eclipse.Feature;
import org.eclipse.equinox.p2.publisher.eclipse.FeatureEntry;
import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.tools.comparator.IArtifactComparator;
import org.eclipse.osgi.util.NLS;

/**
 * An artifact comparator that compares two JAR files. Class files are disassembled 
 * and compared for equivalence, properties and manifest files are compared as such, 
 * all other files are compared byte-for-byte. 
 */
public class JarComparator implements IArtifactComparator {

	private static class FeatureEntryWrapper {
		private FeatureEntry entry;

		public FeatureEntryWrapper(FeatureEntry entry) {
			this.entry = entry;
		}

		@Override
		public boolean equals(Object o) {
			FeatureEntry otherEntry = (o instanceof FeatureEntryWrapper) ? ((FeatureEntryWrapper) o).getEntry() : null;

			if (otherEntry == null || !entry.equals(otherEntry))
				return false;

			String arch = otherEntry.getArch();
			if (arch == null ? entry.getArch() != null : !arch.equals(entry.getArch()))
				return false;
			String os = otherEntry.getOS();
			if (os == null ? entry.getOS() != null : !os.equals(entry.getOS()))
				return false;
			String ws = otherEntry.getWS();
			if (ws == null ? entry.getWS() != null : !ws.equals(entry.getWS()))
				return false;

			return true;
		}

		@Override
		public int hashCode() {
			int hash = entry.hashCode();
			if (entry.getArch() != null)
				hash += entry.getArch().hashCode();
			if (entry.getOS() != null)
				hash += entry.getOS().hashCode();
			if (entry.getWS() != null)
				hash += entry.getWS().hashCode();
			return hash;
		}

		public FeatureEntry getEntry() {
			return entry;
		}
	}

	private static final String LINE_SEPARATOR = "\n"; //$NON-NLS-1$
	private static final String CLASS_EXTENSION = ".class"; //$NON-NLS-1$
	private static final String JAR_EXTENSION = ".jar"; //$NON-NLS-1$
	private static final String PROPERTIES_EXTENSION = ".properties"; //$NON-NLS-1$
	private static final String MAPPINGS_EXTENSION = ".mappings"; //$NON-NLS-1$
	private static final String PLUGIN_ID = "org.eclipse.equinox.p2.repository.tools"; //$NON-NLS-1$
	private static final String DESTINATION_ARTIFACT_PREFIX = "destinationartifact"; //$NON-NLS-1$
	private static final String SUFFIX_JAR = ".jar"; //$NON-NLS-1$
	private static final String SOURCE_ARTIFACT_PREFIX = "sourceartifact"; //$NON-NLS-1$
	private static final String OSGI_BUNDLE_CLASSIFIER = "osgi.bundle"; //$NON-NLS-1$
	private static final String FEATURE_CLASSIFIER = "org.eclipse.update.feature"; //$NON-NLS-1$

	private static final String META_INF = "meta-inf/"; //$NON-NLS-1$
	private static final String DSA_EXT = ".dsa"; //$NON-NLS-1$
	private static final String RSA_EXT = ".rsa"; //$NON-NLS-1$
	private static final String SF_EXT = ".sf"; //$NON-NLS-1$

	private String sourceLocation, destinationLocation, descriptorString;

	public IStatus compare(IArtifactRepository source, IArtifactDescriptor sourceDescriptor, IArtifactRepository destination, IArtifactDescriptor destinationDescriptor) {
		// Cache information for potential error messages
		sourceLocation = URIUtil.toUnencodedString(sourceDescriptor.getRepository().getLocation());
		destinationLocation = URIUtil.toUnencodedString(destinationDescriptor.getRepository().getLocation());
		descriptorString = sourceDescriptor.toString();

		String classifier1 = sourceDescriptor.getArtifactKey().getClassifier();
		String classifier2 = destinationDescriptor.getArtifactKey().getClassifier();
		if (!classifier1.equals(classifier2) || (!OSGI_BUNDLE_CLASSIFIER.equals(classifier1) && !FEATURE_CLASSIFIER.equals(classifier1))) {
			return Status.OK_STATUS;
		}

		File firstTempFile = null;
		File secondTempFile = null;
		try {
			firstTempFile = getLocalJarFile(source, sourceDescriptor, SOURCE_ARTIFACT_PREFIX);
			secondTempFile = getLocalJarFile(destination, destinationDescriptor, DESTINATION_ARTIFACT_PREFIX);
			if (classifier1.equals(OSGI_BUNDLE_CLASSIFIER))
				return compare(firstTempFile, secondTempFile);
			else if (classifier1.equals(FEATURE_CLASSIFIER))
				return compareFeatures(firstTempFile, secondTempFile);
		} catch (CoreException e) {
			return e.getStatus();
		} finally {
			if (firstTempFile != null)
				firstTempFile.delete();
			if (secondTempFile != null)
				secondTempFile.delete();
		}
		return Status.OK_STATUS;
	}

	public IStatus compareFeatures(File sourceFile, File destinationFile) {
		FeatureParser parser = new FeatureParser();
		Feature feature1 = parser.parse(sourceFile);
		Feature feature2 = parser.parse(destinationFile);

		MultiStatus parent = new MultiStatus(PLUGIN_ID, 0, NLS.bind(Messages.differentEntry, new String[] {descriptorString, sourceLocation, destinationLocation}), null);

		if (!feature1.getId().equals(feature2.getId()))
			parent.add(newErrorStatus(NLS.bind(Messages.featureIdsDontMatch, feature1.getId(), feature2.getId())));
		if (!feature1.getVersion().equals(feature2.getVersion()))
			parent.add(newErrorStatus(NLS.bind(Messages.featureVersionsDontMatch, feature1.getVersion(), feature2.getVersion())));

		Map<FeatureEntryWrapper, FeatureEntry> entryMap = new HashMap<FeatureEntryWrapper, FeatureEntry>();
		FeatureEntry[] entries1 = feature1.getEntries();
		FeatureEntry[] entries2 = feature2.getEntries();

		if (entries1.length != entries2.length)
			parent.add(newErrorStatus(Messages.featureSize));

		for (int i = 0; i < entries1.length; i++)
			entryMap.put(new FeatureEntryWrapper(entries1[i]), entries1[i]);

		for (int i = 0; i < entries2.length; i++) {
			FeatureEntry firstEntry = entryMap.get(new FeatureEntryWrapper(entries2[i]));
			if (firstEntry == null)
				parent.add(newErrorStatus(NLS.bind(Messages.featureEntry, entries2[i])));
			else {
				if (firstEntry.isOptional() != entries2[i].isOptional())
					parent.add(newErrorStatus(NLS.bind(Messages.featureEntryOptional, entries2[i])));
				if (firstEntry.isUnpack() != entries2[i].isUnpack())
					parent.add(newErrorStatus(NLS.bind(Messages.featureEntryUnpack, entries2[i])));
				if (firstEntry.isRequires() && firstEntry.getMatch() != null && !firstEntry.getMatch().equals(entries2[i].getMatch()))
					parent.add(newErrorStatus(NLS.bind(Messages.featureEntryMatch, entries2[i])));
				if (firstEntry.getFilter() != null && !firstEntry.getFilter().equals(entries2[i].getFilter()))
					parent.add(newErrorStatus(NLS.bind(Messages.featureEntryFilter, entries2[i])));
			}
		}

		return parent.getChildren().length == 0 ? Status.OK_STATUS : parent;
	}

	@SuppressWarnings("resource")
	public IStatus compare(File sourceFile, File destinationFile) {
		ZipFile firstFile = null;
		ZipFile secondFile = null;
		try {
			firstFile = new ZipFile(sourceFile);
			secondFile = new ZipFile(destinationFile);
			final int firstFileSize = firstFile.size();
			final int secondFileSize = secondFile.size();
			MultiStatus parent = new MultiStatus(PLUGIN_ID, 0, NLS.bind(Messages.differentEntry, new String[] {descriptorString, sourceLocation, destinationLocation}), null);

			if (firstFileSize != secondFileSize) {
				parent.add(newErrorStatus(NLS.bind(Messages.differentNumberOfEntries, new String[] {descriptorString, sourceLocation, Integer.toString(firstFileSize), destinationLocation, Integer.toString(secondFileSize)})));
				return parent;
			}
			for (Enumeration<? extends ZipEntry> enumeration = firstFile.entries(); enumeration.hasMoreElements();) {
				ZipEntry entry = enumeration.nextElement();
				String entryName = entry.getName();
				final ZipEntry entry2 = secondFile.getEntry(entryName);
				IStatus result = null;
				if (!entry.isDirectory() && entry2 != null) {
					String lowerCase = entryName.toLowerCase();
					if (isSigningEntry(lowerCase)) {
						continue;
					}

					InputStream firstStream = null;
					InputStream secondStream = null;
					try {
						firstStream = new BufferedInputStream(firstFile.getInputStream(entry));
						secondStream = new BufferedInputStream(secondFile.getInputStream(entry2));
						if (lowerCase.endsWith(CLASS_EXTENSION)) {
							result = compareClasses(entryName, firstStream, entry.getSize(), secondStream, entry2.getSize());
						} else if (lowerCase.endsWith(JAR_EXTENSION)) {
							result = compareNestedJars(firstStream, entry.getSize(), secondStream, entry2.getSize(), entryName);
						} else if (lowerCase.endsWith(PROPERTIES_EXTENSION) || lowerCase.endsWith(MAPPINGS_EXTENSION)) {
							result = compareProperties(entryName, firstStream, secondStream);
						} else if (entryName.equalsIgnoreCase(JarFile.MANIFEST_NAME)) {
							result = compareManifest(firstStream, secondStream); //MANIFEST.MF file
						} else {
							long size1 = entry.getSize();
							long size2 = entry2.getSize();
							if (size1 != size2)
								result = newErrorStatus(NLS.bind(Messages.binaryDifferentLength, new String[] {entryName, String.valueOf(Math.abs(size1 - size2))}));
							else
								result = compareBytes(entryName, firstStream, entry.getSize(), secondStream, entry2.getSize());
						}
					} finally {
						Utility.close(firstStream);
						Utility.close(secondStream);
					}
				} else if (!entry.isDirectory()) {
					// missing entry, entry2 == null
					result = newErrorStatus(NLS.bind(Messages.missingEntry, new String[] {entryName, descriptorString, sourceLocation}));
				}

				if (result != null && !result.isOK()) {
					parent.add(result);
					return parent;
				}
			}
		} catch (IOException e) {
			// missing entry
			return newErrorStatus(NLS.bind(Messages.ioexception, new String[] {sourceFile.getAbsolutePath(), destinationFile.getAbsolutePath()}), e);
		} finally {
			Utility.close(firstFile);
			Utility.close(secondFile);
		}
		return Status.OK_STATUS;
	}

	private IStatus compareManifest(InputStream firstStream, InputStream secondStream) throws IOException {
		Manifest manifest = new Manifest(firstStream);
		Manifest manifest2 = new Manifest(secondStream);

		if (manifest == null || manifest2 == null)
			return Status.OK_STATUS;

		Attributes attributes = manifest.getMainAttributes();
		Attributes attributes2 = manifest2.getMainAttributes();
		if (attributes.size() != attributes2.size())
			return newErrorStatus(NLS.bind(Messages.manifestDifferentSize, String.valueOf(Math.abs(attributes.size() - attributes2.size()))));
		for (Entry<Object, Object> entry : attributes.entrySet()) {
			Object value2 = attributes2.get(entry.getKey());
			if (value2 == null) {
				return newErrorStatus(NLS.bind(Messages.manifestMissingEntry, entry.getKey()));
			}
			if (!value2.equals(entry.getValue())) {
				return newErrorStatus(NLS.bind(Messages.manifestDifferentValue, entry.getKey()));
			}
		}
		return Status.OK_STATUS;
	}

	private IStatus compareClasses(String entryName, InputStream stream1, long size1, InputStream stream2, long size2) throws IOException {
		Disassembler disassembler = new Disassembler();
		byte[] firstEntryClassFileBytes = Utility.getInputStreamAsByteArray(stream1, (int) size1);
		byte[] secondEntryClassFileBytes = Utility.getInputStreamAsByteArray(stream2, (int) size2);

		String contentsFile1 = null;
		try {
			contentsFile1 = disassembler.disassemble(firstEntryClassFileBytes, LINE_SEPARATOR, Disassembler.DETAILED | Disassembler.COMPACT);
		} catch (ClassFormatException e) {
			// ignore
		}
		String contentsFile2 = null;
		try {
			contentsFile2 = disassembler.disassemble(secondEntryClassFileBytes, LINE_SEPARATOR, Disassembler.DETAILED | Disassembler.COMPACT);
		} catch (ClassFormatException e) {
			// ignore
		}
		if (contentsFile1 == null || contentsFile2 == null) {
			// one of the two .class file (or both) is corrupted
			if (contentsFile1 == null) {
				if (contentsFile2 != null) {
					// first .class file is corrupted and not the second one
					return newErrorStatus(NLS.bind(Messages.classesDifferent, entryName));
				}
				// both .class files are corrupted and we need to do a byte comparison in case the .class file is corrupted on purpose
				if (!Arrays.equals(firstEntryClassFileBytes, secondEntryClassFileBytes)) {
					return newErrorStatus(NLS.bind(Messages.binaryFilesDifferent, entryName));
				}
				return Status.OK_STATUS;
			}
			// first .class file is not corrupted but the second one is
			return newErrorStatus(NLS.bind(Messages.classesDifferent, entryName));
		}
		if (!contentsFile1.equals(contentsFile2)) {
			return newErrorStatus(NLS.bind(Messages.classesDifferent, entryName));
		}
		return Status.OK_STATUS;
	}

	private IStatus compareNestedJars(InputStream stream1, long size1, InputStream stream2, long size2, String entry) throws IOException {
		File firstTempFile = getLocalJarFile(stream1, entry, size1);
		File secondTempFile = getLocalJarFile(stream2, entry, size2);

		try {
			return compare(firstTempFile, secondTempFile);
		} finally {
			if (firstTempFile != null)
				firstTempFile.delete();
			if (secondTempFile != null)
				secondTempFile.delete();
		}
	}

	private IStatus compareProperties(String entryName, InputStream stream1, InputStream stream2) {
		Properties props1 = loadProperties(stream1);
		Properties props2 = loadProperties(stream2);
		if (props1.size() != props2.size())
			return newErrorStatus(NLS.bind(Messages.propertiesSizesDifferent, entryName, String.valueOf(Math.abs(props1.size() - props2.size()))));

		props1.keys();
		for (Iterator<Object> iterator = props1.keySet().iterator(); iterator.hasNext();) {
			String key = (String) iterator.next();
			if (!props2.containsKey(key))
				return newErrorStatus(NLS.bind(Messages.missingProperty, key, entryName));
			String prop1 = props1.getProperty(key);
			String prop2 = props2.getProperty(key);
			if (!prop1.equals(prop2)) {
				if (prop1.length() < 15 && prop2.length() < 15)
					return newErrorStatus(NLS.bind(Messages.differentPropertyValueFull, new String[] {entryName, key, prop1, prop2}));
				// strings are too long, report the first bit that is different
				String[] diff = extractDifference(prop1, prop2);
				return newErrorStatus(NLS.bind(Messages.differentPropertyValueFull, new String[] {entryName, key, diff[0], diff[1]}));
			}

		}
		return Status.OK_STATUS;
	}

	/*
	 * Given two different strings return the first segments of those
	 * strings that illustrate the differences.
	 */
	private String[] extractDifference(String s1, String s2) {
		for (int i = 0; i < s1.length() && i < s2.length(); i++) {
			if (s1.charAt(i) != s2.charAt(i)) {
				String result1, result2;
				boolean truncated;
				if (i > 3) {
					truncated = (i + 7) < s1.length();
					result1 = "..." + s1.substring(i - 3, truncated ? i + 7 : s1.length()) + (truncated ? "..." : ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					truncated = (i + 7) < s2.length();
					result2 = "..." + s2.substring(i - 3, truncated ? i + 7 : s2.length()) + (truncated ? "..." : ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				} else {
					truncated = (i + 10) < s1.length();
					result1 = s1.substring(0, truncated ? i + 10 : s1.length()) + (truncated ? "..." : ""); //$NON-NLS-1$ //$NON-NLS-2$
					truncated = (i + 10) < s2.length();
					result2 = s2.substring(0, truncated ? i + 10 : s2.length()) + (truncated ? "..." : ""); //$NON-NLS-1$ //$NON-NLS-2$
				}
				return new String[] {result1, result2};
			}
		}
		//no differences?
		return new String[] {s1, s2};
	}

	private IStatus compareBytes(String entryName, InputStream firstStream, long size1, InputStream secondStream, long size2) throws IOException {
		byte[] firstBytes = Utility.getInputStreamAsByteArray(firstStream, (int) size1);
		byte[] secondBytes = Utility.getInputStreamAsByteArray(secondStream, (int) size2);
		if (!Arrays.equals(firstBytes, secondBytes))
			return newErrorStatus(NLS.bind(Messages.binaryFilesDifferent, entryName));
		return Status.OK_STATUS;
	}

	private Properties loadProperties(InputStream input) {
		Properties result = new Properties();
		try {
			result.load(input);
		} catch (IOException e) {
			//ignore
		}
		return result;
	}

	private String normalize(String entryName) {
		StringBuffer buffer = new StringBuffer();
		char[] chars = entryName.toCharArray();
		for (int i = 0, max = chars.length; i < max; i++) {
			char currentChar = chars[i];
			if (!Character.isJavaIdentifierPart(currentChar)) {
				buffer.append('_');
			} else {
				buffer.append(currentChar);
			}
		}
		return String.valueOf(buffer);
	}

	private IStatus newErrorStatus(String message, Exception e) {
		return new Status(IStatus.ERROR, PLUGIN_ID, message, e);
	}

	private IStatus newErrorStatus(String message) {
		return newErrorStatus(message, null);
	}

	private File getLocalJarFile(IArtifactRepository repository, IArtifactDescriptor descriptor, String prefix) throws CoreException {
		File file = null;
		BufferedOutputStream stream = null;
		try {
			file = File.createTempFile(prefix, SUFFIX_JAR);
			stream = new BufferedOutputStream(new FileOutputStream(file));
			IStatus status = repository.getArtifact(descriptor, stream, new NullProgressMonitor());
			if (!status.isOK())
				throw new CoreException(status);
			stream.flush();
		} catch (FileNotFoundException e) {
			throw new CoreException(newErrorStatus("FileNotFoundException", e)); //$NON-NLS-1$
		} catch (IOException e) {
			throw new CoreException(newErrorStatus("IOException", e)); //$NON-NLS-1$
		} finally {
			Utility.close(stream);
		}
		return file;
	}

	private File getLocalJarFile(InputStream inputStream, String entry, long size) throws IOException {
		byte[] firstEntryClassFileBytes = Utility.getInputStreamAsByteArray(inputStream, (int) size);

		File tempFile = null;
		BufferedOutputStream stream = null;
		try {
			tempFile = File.createTempFile(SOURCE_ARTIFACT_PREFIX + normalize(entry), SUFFIX_JAR);
			stream = new BufferedOutputStream(new FileOutputStream(tempFile));
			stream.write(firstEntryClassFileBytes);
			stream.flush();
		} finally {
			Utility.close(stream);
		}
		return tempFile;
	}

	private boolean isSigningEntry(String entry) {
		return (entry.startsWith(META_INF) && (entry.endsWith(SF_EXT) || entry.endsWith(RSA_EXT) || entry.endsWith(DSA_EXT)));
	}
}

Back to the top