| /******************************************************************************* |
| * Copyright (c) 2015, 2016 Google, Inc 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: |
| * Stefan Xenos (Google) - Initial implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.core.nd.java; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| |
| import org.eclipse.core.filesystem.EFS; |
| import org.eclipse.core.filesystem.IFileInfo; |
| import org.eclipse.core.filesystem.IFileStore; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.jdt.internal.core.nd.StreamHasher; |
| |
| public class FileFingerprint { |
| /** |
| * Sentinel value for {@link #time} indicating a nonexistent fingerprint. This is used for the timestamp of |
| * nonexistent files and for the {@link #getEmpty()} singleton. |
| */ |
| public static final long NEVER_MODIFIED = 0; |
| |
| /** |
| * Sentinel value for {@link #time} indicating that the timestamp was not recorded as part of the fingerprint. |
| * This is normally used to indicate that the file's timestamp was so close to the current system time at the time |
| * the fingerprint was computed that subsequent changes in the file might not be detected. In such cases, timestamps |
| * are an unreliable method for determining if the file has changed and so are not included as part of the fingerprint. |
| */ |
| public static final long UNKNOWN = 1; |
| |
| /** |
| * Worst-case accuracy of filesystem timestamps, among all supported platforms (this is currently 1s on linux, 2s on |
| * FAT systems). |
| */ |
| private static final long WORST_FILESYSTEM_TIMESTAMP_ACCURACY_MS = 2000; |
| |
| private long time; |
| private long hash; |
| private long size; |
| |
| private static final FileFingerprint EMPTY = new FileFingerprint(NEVER_MODIFIED,0,0); |
| |
| public static final FileFingerprint getEmpty() { |
| return EMPTY; |
| } |
| |
| public static final FileFingerprint create(IPath path, IProgressMonitor monitor) throws CoreException { |
| return getEmpty().test(path, monitor).getNewFingerprint(); |
| } |
| |
| public FileFingerprint(long time, long size, long hash) { |
| super(); |
| this.time = time; |
| this.size = size; |
| this.hash = hash; |
| } |
| |
| public long getTime() { |
| return this.time; |
| } |
| |
| public long getHash() { |
| return this.hash; |
| } |
| |
| public long getSize() { |
| return this.size; |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + (int) (this.hash ^ (this.hash >>> 32)); |
| result = prime * result + (int) (this.size ^ (this.size >>> 32)); |
| result = prime * result + (int) (this.time ^ (this.time >>> 32)); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) |
| return true; |
| if (obj == null) |
| return false; |
| if (getClass() != obj.getClass()) |
| return false; |
| FileFingerprint other = (FileFingerprint) obj; |
| if (this.hash != other.hash) |
| return false; |
| if (this.size != other.size) |
| return false; |
| if (this.time != other.time) |
| return false; |
| return true; |
| } |
| |
| public static class FingerprintTestResult { |
| private boolean matches; |
| private boolean needsNewFingerprint; |
| private FileFingerprint newFingerprint; |
| |
| public FingerprintTestResult(boolean matches, boolean needsNewFingerprint, FileFingerprint newFingerprint) { |
| super(); |
| this.matches = matches; |
| this.newFingerprint = newFingerprint; |
| this.needsNewFingerprint = needsNewFingerprint; |
| } |
| |
| public boolean needsNewFingerprint() { |
| return this.needsNewFingerprint; |
| } |
| |
| public boolean matches() { |
| return this.matches; |
| } |
| |
| public FileFingerprint getNewFingerprint() { |
| return this.newFingerprint; |
| } |
| |
| @Override |
| public String toString() { |
| return "FingerprintTestResult [matches=" + this.matches + ", needsNewFingerprint=" //$NON-NLS-1$//$NON-NLS-2$ |
| + this.needsNewFingerprint + ", newFingerprint=" + this.newFingerprint + "]"; //$NON-NLS-1$//$NON-NLS-2$ |
| } |
| } |
| |
| /** |
| * Returns true iff the file existed at the time the fingerprint was computed. |
| * |
| * @return true iff the file existed at the time the fingerprint was computed. |
| */ |
| public boolean fileExists() { |
| return !equals(EMPTY); |
| } |
| |
| /** |
| * Compares the given File with the receiver. If the fingerprint matches (ie: the file |
| */ |
| public FingerprintTestResult test(IPath path, IProgressMonitor monitor) throws CoreException { |
| SubMonitor subMonitor = SubMonitor.convert(monitor, 100); |
| long currentTime = System.currentTimeMillis(); |
| IFileStore store = EFS.getLocalFileSystem().getStore(path); |
| IFileInfo fileInfo = store.fetchInfo(); |
| |
| long lastModified = fileInfo.getLastModified(); |
| if (Math.abs(currentTime - lastModified) < WORST_FILESYSTEM_TIMESTAMP_ACCURACY_MS) { |
| // If the file was modified so recently that it's within our ability to measure it, don't include |
| // the timestamp as part of the fingerprint. If another change were to happen to the file immediately |
| // afterward, we might not be able to detect it using the timestamp. |
| lastModified = UNKNOWN; |
| } |
| subMonitor.split(5); |
| |
| long fileSize = fileInfo.getLength(); |
| subMonitor.split(5); |
| if (lastModified != UNKNOWN && lastModified == this.time && fileSize == this.size) { |
| return new FingerprintTestResult(true, false, this); |
| } |
| |
| long hashCode; |
| try { |
| hashCode = fileSize == 0 ? 0 : computeHashCode(path.toFile(), fileSize, subMonitor.split(90)); |
| } catch (IOException e) { |
| throw new CoreException(Package.createStatus("An error occurred computing a hash code", e)); //$NON-NLS-1$ |
| } |
| boolean matches = (hashCode == this.hash && fileSize == this.size); |
| |
| FileFingerprint newFingerprint = new FileFingerprint(lastModified, fileSize, hashCode); |
| return new FingerprintTestResult(matches, !equals(newFingerprint), newFingerprint); |
| } |
| |
| private long computeHashCode(File toTest, long fileSize, IProgressMonitor monitor) throws IOException { |
| final int BUFFER_SIZE = 2048; |
| char[] charBuffer = new char[BUFFER_SIZE]; |
| byte[] byteBuffer = new byte[BUFFER_SIZE * 2]; |
| |
| SubMonitor subMonitor = SubMonitor.convert(monitor, (int) (fileSize / (BUFFER_SIZE * 2))); |
| StreamHasher hasher = new StreamHasher(); |
| try { |
| InputStream inputStream = new FileInputStream(toTest); |
| try { |
| while (true) { |
| subMonitor.split(1); |
| int bytesRead = readUntilBufferFull(inputStream, byteBuffer); |
| |
| if (bytesRead < byteBuffer.length) { |
| charBuffer = new char[(bytesRead + 1) / 2]; |
| copyByteArrayToCharArray(charBuffer, byteBuffer, bytesRead); |
| hasher.addChunk(charBuffer); |
| break; |
| } |
| |
| copyByteArrayToCharArray(charBuffer, byteBuffer, bytesRead); |
| hasher.addChunk(charBuffer); |
| } |
| } finally { |
| inputStream.close(); |
| } |
| |
| } catch (FileNotFoundException e) { |
| return 0; |
| } |
| |
| return hasher.computeHash(); |
| } |
| |
| private void copyByteArrayToCharArray(char[] charBuffer, byte[] byteBuffer, int bytesToCopy) { |
| for (int ch = 0; ch < bytesToCopy / 2; ch++) { |
| char next = (char) (byteBuffer[ch * 2] + byteBuffer[ch * 2 + 1]); |
| charBuffer[ch] = next; |
| } |
| |
| if (bytesToCopy % 2 != 0) { |
| charBuffer[bytesToCopy / 2] = (char) byteBuffer[bytesToCopy - 1]; |
| } |
| } |
| |
| int readUntilBufferFull(InputStream inputStream, byte[] buffer) throws IOException { |
| int bytesRead = 0; |
| while (bytesRead < buffer.length) { |
| int thisRead = inputStream.read(buffer, bytesRead, buffer.length - bytesRead); |
| |
| if (thisRead == -1) { |
| return bytesRead; |
| } |
| |
| bytesRead += thisRead; |
| } |
| return bytesRead; |
| } |
| |
| private static String getTimeString(long timestamp) { |
| if (timestamp == UNKNOWN) { |
| return "UNKNOWN"; //$NON-NLS-1$ |
| } else if (timestamp == NEVER_MODIFIED) { |
| return "NEVER_MODIFIED"; //$NON-NLS-1$ |
| } |
| return Long.toString(timestamp); |
| } |
| |
| @Override |
| public String toString() { |
| return "FileFingerprint [time=" + getTimeString(this.time) + ", size=" + this.size + ", hash=" + this.hash + "]"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$ |
| } |
| } |