blob: 87f3ea63867abe5bd689c742874435e25f9e01c4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2016 Igor Fedorenko
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Igor Fedorenko - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.launching.sourcelookup.advanced;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Helpers to compute file content digests. Provides long-lived hasher instance with bounded cache of most recently requested files, which is useful
* to handle source lookup requests. Also provides factory of hasher instances with unbounded caches, which is useful to perform bulk workspace
* indexing.
*/
public class FileHashing {
public static interface Hasher {
Object hash(File file);
}
// default hasher with bounded cache.
// this is used when performing source lookup and number of unique files requested during the same debugging session is likely to be small.
private static final HasherImpl HASHER = new HasherImpl(5000);
/**
* Returns default long-lived Hasher instance with bounded hash cache.
*/
public static Hasher hasher() {
return HASHER;
}
/**
* Returns new Hasher instance with unbounded hash cache, useful for bulk hashing of projects and their dependencies.
*/
public static Hasher newHasher() {
return new HasherImpl(HASHER);
}
private static class CacheKey {
public final File file;
private final long length;
private final long lastModified;
public CacheKey(File file) throws IOException {
this.file = file.getCanonicalFile();
this.length = file.length();
this.lastModified = file.lastModified();
}
@Override
public int hashCode() {
int hash = 17;
hash = hash * 31 + file.hashCode();
hash = hash * 31 + (int) length;
hash = hash * 31 + (int) lastModified;
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof CacheKey)) {
return false;
}
CacheKey other = (CacheKey) obj;
return file.equals(other.file) && length == other.length && lastModified == other.lastModified;
}
}
private static class HashCode {
private final byte[] bytes;
public HashCode(byte[] bytes) {
this.bytes = bytes; // assumes this class "owns" the array from now on
}
@Override
public int hashCode() {
return Arrays.hashCode(bytes);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof HashCode)) {
return false;
}
return Arrays.equals(bytes, ((HashCode) obj).bytes);
}
@Override
public final String toString() {
StringBuilder sb = new StringBuilder(2 * bytes.length);
for (byte b : bytes) {
sb.append(hexDigits[(b >> 4) & 0xf]).append(hexDigits[b & 0xf]);
}
return sb.toString();
}
private static final char[] hexDigits = "0123456789abcdef".toCharArray(); //$NON-NLS-1$
}
private static class HasherImpl implements Hasher {
private final Map<CacheKey, HashCode> cache;
@SuppressWarnings("serial")
public HasherImpl(int cacheSize) {
this.cache = new LinkedHashMap<CacheKey, HashCode>() {
@Override
protected boolean removeEldestEntry(Map.Entry<CacheKey, HashCode> eldest) {
return size() > cacheSize;
}
};
}
public HasherImpl(HasherImpl initial) {
this.cache = new LinkedHashMap<>(initial.cache);
}
@Override
public Object hash(File file) {
if (file == null || !file.isFile()) {
return null;
}
try {
CacheKey cacheKey = new CacheKey(file);
synchronized (cache) {
HashCode hashCode = cache.get(cacheKey);
if (hashCode != null) {
return hashCode;
}
}
// don't hold cache lock while hashing file
HashCode hashCode = sha1(file);
synchronized (cache) {
cache.put(cacheKey, hashCode);
}
return hashCode;
}
catch (IOException e) {
return null; // file does not exist or can't be read
}
}
}
private static HashCode sha1(File file) throws IOException {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA1"); //$NON-NLS-1$
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Unsupported JVM", e); //$NON-NLS-1$
}
byte[] buf = new byte[4096];
try (InputStream is = new FileInputStream(file)) {
int len;
while ((len = is.read(buf)) > 0) {
digest.update(buf, 0, len);
}
}
return new HashCode(digest.digest());
}
}