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