/******************************************************************************* * Copyright (c) 2004, 2006 * Thomas Hallgren, Kenneth Olwing, Mitch Sonies * Pontus Rydin, Nils Unden, Peer Torngren * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the individual * copyright holders listed above, as Initial Contributors under such license. * The text of such license is available at www.eclipse.org. *******************************************************************************/ package org.eclipse.buckminster.manifest; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import org.eclipse.buckminster.core.helpers.FileUtils; import org.eclipse.buckminster.core.helpers.NullOutputStream; import org.eclipse.buckminster.runtime.MonitorUtils; import org.eclipse.buckminster.runtime.MultiTeeOutputStream; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; public class Manifest { private static final String HEADER = "MANIFEST:v0"; private static final String DESCRIPTION_PREFIX = "DESCRIPTION:"; private static final String CHECKSUMS_PREFIX = "CHECKSUMS:"; private static final String ENTRIES_PREFIX = "ENTRIES:"; public static Manifest create(File fromRoot) throws NoSuchAlgorithmException, IOException, ChecksumMismatchException, PathMismatchException { return Manifest.create(fromRoot, null, null, null, null); } public static Manifest create(File fromRoot, String algorithm, String assumedLineSeparator, String description, IProgressMonitor monitor) throws IOException, NoSuchAlgorithmException, ChecksumMismatchException, PathMismatchException { if (algorithm == null) algorithm = Constants.DEFAULT_ALGORITHM; if (assumedLineSeparator == null) assumedLineSeparator = Constants.LOCAL_LINESEPARATOR; File canonicalFromRoot = fromRoot.getCanonicalFile(); if (description == null) description = canonicalFromRoot.getAbsolutePath(); if (monitor == null) monitor = new NullProgressMonitor(); try { monitor.beginTask(null, IProgressMonitor.UNKNOWN); MessageDigest md = MessageDigest.getInstance(algorithm); DigestOutputStream manifestDigestStream = new DigestOutputStream(NullOutputStream.INSTANCE, md); List<PathEntry> entries = new ArrayList<PathEntry>(); recursiveDigests(canonicalFromRoot, algorithm, assumedLineSeparator, manifestDigestStream, canonicalFromRoot, entries, monitor); manifestDigestStream.close(); Checksum checksum = new Checksum(md.digest(), algorithm, assumedLineSeparator); Manifest mf = new Manifest(description, new Checksum[] { checksum }, entries.toArray(PathEntry.EMPTY_LIST)); MonitorUtils.worked(monitor, 1); return mf; } finally { monitor.done(); } } public static Manifest merge(Manifest left, Manifest right) throws NonmatchingManifestsException, ChecksumMismatchException { String description = new StringBuilder(left.getDescription()).append(" merged with ").append( right.getDescription()).toString(); if (left.equals(right)) return new Manifest(description, left.getAllChecksums(), left.getAllEntries()); if (new Difference(left, right).getResult() == Difference.RESULT.NONMATCHING) throw new NonmatchingManifestsException(left, right); Set<Checksum> mfChecksums = new TreeSet<Checksum>(); mfChecksums.addAll(Arrays.asList(left.getAllChecksums())); mfChecksums.addAll(Arrays.asList(right.getAllChecksums())); Map<String, PathEntry> entryMap = new HashMap<String, PathEntry>(); for (PathEntry leftEntry : left.getAllEntries()) entryMap.put(leftEntry.getName(), leftEntry); for (PathEntry rightEntry : right.getAllEntries()) { PathEntry existingEntry = entryMap.get(rightEntry.getName()); assert existingEntry != null : "Unexpectedly null"; existingEntry.addChecksums(rightEntry.getAllChecksums()); } return new Manifest(description, mfChecksums.toArray(Checksum.EMPTY_LIST), entryMap.values().toArray( PathEntry.EMPTY_LIST)); } public static Manifest fromBufferedReader(BufferedReader br, String description) throws MissingDataException, IOException, ChecksumMismatchException { String tmp; if (!br.readLine().equals(HEADER)) throw new MissingDataException("Missing header in persisted Manifest"); tmp = br.readLine(); if (!tmp.startsWith(DESCRIPTION_PREFIX)) throw new MissingDataException("Missing description in persisted Manifest"); if (description == null) description = tmp.substring(DESCRIPTION_PREFIX.length()) + " (persisted)"; tmp = br.readLine(); if (!tmp.startsWith(CHECKSUMS_PREFIX)) throw new MissingDataException("Missing checksums in persisted Manifest"); Checksum[] checksums = new Checksum[Integer.parseInt(tmp.substring(CHECKSUMS_PREFIX.length()))]; for (int i = 0; i < checksums.length; i++) checksums[i] = Checksum.fromBufferedReader(br); tmp = br.readLine(); if (!tmp.startsWith(ENTRIES_PREFIX)) throw new MissingDataException("Missing entries in persisted Manifest"); PathEntry[] entries = new PathEntry[Integer.parseInt(tmp.substring(ENTRIES_PREFIX.length()))]; for (int i = 0; i < entries.length; i++) entries[i] = PathEntry.fromBufferedReader(br); return new Manifest(description, checksums, entries); } private static void recursiveDigests(File dir, String algorithm, String assumedLineSeparator, OutputStream parentDigestStream, File topRoot, List<PathEntry> entries, IProgressMonitor monitor) throws IOException, NoSuchAlgorithmException, ChecksumMismatchException, PathMismatchException { // get the file list, but *always* sort it to ensure we // always process things in the same order as this is important // for always getting the same digest // File files[] = dir.listFiles(); Arrays.sort(files); for (File f : files) { // Make names count for everything we encounter to ensure // changes in dir structure also count (new dirs, empty files, // changed names). Specifically, write something extra for dirs, // will catch dir swapped for an empty file or vice versa // parentDigestStream.write(f.getName().getBytes()); if (f.isDirectory()) parentDigestStream.write(1); monitor.subTask(f.toString()); MessageDigest md = MessageDigest.getInstance(algorithm); DigestOutputStream digestStream = new DigestOutputStream(NullOutputStream.INSTANCE, md); // the mtos should *not* be closed, only flushed MultiTeeOutputStream mtos = new MultiTeeOutputStream( new OutputStream[] { digestStream, parentDigestStream }); if (f.isFile()) { FileInputStream fis = new FileInputStream(f); FileUtils.copyFile(fis, mtos, MonitorUtils.subMonitor(monitor, 1)); fis.close(); } else recursiveDigests(f, algorithm, assumedLineSeparator, mtos, topRoot, entries, monitor); mtos.flush(); digestStream.close(); entries.add(new PathEntry(f, topRoot, new Checksum[] { new Checksum(md.digest(), algorithm, assumedLineSeparator) })); MonitorUtils.worked(monitor, 1); } } private final String m_description; // ========== // NOTE: These members are used for equals/hashCode calculations // If you in any way change, delete or add to them, ensure to // update equals/hashCode to account for it. // private Checksum[] m_checksums = Checksum.EMPTY_LIST; private final SortedMap<String, PathEntry> m_entryMap = new TreeMap<String, PathEntry>(); // must only be created through static methods private Manifest(String description, Checksum[] checksums, PathEntry[] entries) throws ChecksumMismatchException { m_description = description; this.addChecksums(checksums); for (PathEntry entry : entries) m_entryMap.put(entry.getName(), entry); } /* package */void addChecksums(Checksum[] checksums) throws ChecksumMismatchException { // only add checksums if we don't have them // if an attempt is made to add a checksum we do have, ensure the // existing one really is equal to existing // List<Checksum> checksums2Add = new ArrayList<Checksum>(); for (Checksum c2add : checksums) { Checksum existing = this.internalGetChecksum(c2add.getAlgorithm(), c2add.getAssumedLineSeparator(), checksums2Add); if (existing == null) checksums2Add.add(c2add); else if (!c2add.equals(existing)) throw new ChecksumMismatchException(existing, c2add); } int sz = checksums2Add.size(); if (sz > 0) { Checksum[] newChecksums = new Checksum[m_checksums.length + sz]; System.arraycopy(m_checksums, 0, newChecksums, 0, m_checksums.length); System.arraycopy(checksums2Add.toArray(Checksum.EMPTY_LIST), 0, newChecksums, m_checksums.length, sz); m_checksums = newChecksums; Arrays.sort(m_checksums); } } public String getDescription() { return m_description; } public Checksum getChecksum() { return this.getChecksum(null, null); } public Checksum getChecksum(String algorithm, String assumedLineSeparator) { return this.internalGetChecksum(algorithm, assumedLineSeparator, null); } public Checksum[] getAllChecksums() { return m_checksums; } public String[] getAllEntryNames() { return m_entryMap.keySet().toArray(new String[m_entryMap.size()]); } public PathEntry getEntry(String name) { return m_entryMap.get(name); } public PathEntry[] getAllEntries() { return m_entryMap.values().toArray(new PathEntry[m_entryMap.size()]); } public Difference getDifference(Manifest right) { return new Difference(this, right); } @Override public String toString() { StringBuilder sb = new StringBuilder(m_description); sb.append("=>"); sb.append(Arrays.toString(this.getAllChecksums())); sb.append("=>"); sb.append(m_entryMap.size()); return sb.toString(); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Manifest)) return false; Manifest that = (Manifest)o; if (!Arrays.equals(this.getAllChecksums(), that.getAllChecksums())) return false; if(!Arrays.equals(this.getAllEntries(), that.getAllEntries())) return false; return true; } @Override public int hashCode() { int result = 17; result = 37 * result + Arrays.hashCode(this.getAllChecksums()); result = 37 * result + Arrays.hashCode(this.getAllEntries()); return result; } public void toPrintWriter(PrintWriter pw) { pw.println(HEADER); pw.print(DESCRIPTION_PREFIX); pw.println(m_description); pw.print(CHECKSUMS_PREFIX); pw.println(m_checksums.length); for (Checksum c : m_checksums) c.toPrintWriter(pw); pw.print(ENTRIES_PREFIX); pw.println(m_entryMap.size()); for (PathEntry pe : m_entryMap.values()) pe.toPrintWriter(pw); } private Checksum internalGetChecksum(String algorithm, String assumedLineSeparator, List<Checksum> additionalList) { for (Checksum c : m_checksums) if ((algorithm == null || c.getAlgorithm().equals(algorithm)) && (assumedLineSeparator == null || c.getAssumedLineSeparator().equals(assumedLineSeparator))) return c; if (additionalList != null) for (Checksum c : additionalList) if ((algorithm == null || c.getAlgorithm().equals(algorithm)) && (assumedLineSeparator == null || c.getAssumedLineSeparator().equals(assumedLineSeparator))) return c; return null; } }