| /******************************************************************************* |
| * 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; |
| } |
| |
| 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))); |
| } |
| } |