blob: 79741423b6013df791796035b1978beec6ce4952 [file] [log] [blame]
/*******************************************************************************
* 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.aspectj.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.aspectj.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$
}
}