1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.gallery3d.common; 18 19import java.io.IOException; 20import java.io.InputStream; 21import java.security.DigestInputStream; 22import java.security.MessageDigest; 23import java.security.NoSuchAlgorithmException; 24import java.util.Arrays; 25import java.util.List; 26 27/** 28 * MD5-based digest Wrapper. 29 */ 30public class Fingerprint { 31 // Instance of the MessageDigest using our specified digest algorithm. 32 private static final MessageDigest DIGESTER; 33 34 /** 35 * Name of the digest algorithm we use in {@link java.security.MessageDigest} 36 */ 37 private static final String DIGEST_MD5 = "md5"; 38 39 // Version 1 streamId prefix. 40 // Hard coded stream id length limit is 40-chars. Don't ask! 41 private static final String STREAM_ID_CS_PREFIX = "cs_01_"; 42 43 // 16 bytes for 128-bit fingerprint 44 private static final int FINGERPRINT_BYTE_LENGTH; 45 46 // length of prefix + 32 hex chars for 128-bit fingerprint 47 private static final int STREAM_ID_CS_01_LENGTH; 48 49 static { 50 try { 51 DIGESTER = MessageDigest.getInstance(DIGEST_MD5); 52 FINGERPRINT_BYTE_LENGTH = DIGESTER.getDigestLength(); 53 STREAM_ID_CS_01_LENGTH = STREAM_ID_CS_PREFIX.length() 54 + (FINGERPRINT_BYTE_LENGTH * 2); 55 } catch (NoSuchAlgorithmException e) { 56 // can't continue, but really shouldn't happen 57 throw new IllegalStateException(e); 58 } 59 } 60 61 // md5 digest bytes. 62 private final byte[] mMd5Digest; 63 64 /** 65 * Creates a new Fingerprint. 66 */ 67 public Fingerprint(byte[] bytes) { 68 if ((bytes == null) || (bytes.length != FINGERPRINT_BYTE_LENGTH)) { 69 throw new IllegalArgumentException(); 70 } 71 mMd5Digest = bytes; 72 } 73 74 /** 75 * Creates a Fingerprint based on the contents of a file. 76 * 77 * Note that this will close() stream after calculating the digest. 78 * @param byteCount length of original data will be stored at byteCount[0] as a side product 79 * of the fingerprint calculation 80 */ 81 public static Fingerprint fromInputStream(InputStream stream, long[] byteCount) 82 throws IOException { 83 DigestInputStream in = null; 84 long count = 0; 85 try { 86 in = new DigestInputStream(stream, DIGESTER); 87 byte[] bytes = new byte[8192]; 88 while (true) { 89 // scan through file to compute a fingerprint. 90 int n = in.read(bytes); 91 if (n < 0) break; 92 count += n; 93 } 94 } finally { 95 if (in != null) in.close(); 96 } 97 if ((byteCount != null) && (byteCount.length > 0)) byteCount[0] = count; 98 return new Fingerprint(in.getMessageDigest().digest()); 99 } 100 101 /** 102 * Decodes a string stream id to a 128-bit fingerprint. 103 */ 104 public static Fingerprint fromStreamId(String streamId) { 105 if ((streamId == null) 106 || !streamId.startsWith(STREAM_ID_CS_PREFIX) 107 || (streamId.length() != STREAM_ID_CS_01_LENGTH)) { 108 throw new IllegalArgumentException("bad streamId: " + streamId); 109 } 110 111 // decode the hex bytes of the fingerprint portion 112 byte[] bytes = new byte[FINGERPRINT_BYTE_LENGTH]; 113 int byteIdx = 0; 114 for (int idx = STREAM_ID_CS_PREFIX.length(); idx < STREAM_ID_CS_01_LENGTH; 115 idx += 2) { 116 int value = (toDigit(streamId, idx) << 4) | toDigit(streamId, idx + 1); 117 bytes[byteIdx++] = (byte) (value & 0xff); 118 } 119 return new Fingerprint(bytes); 120 } 121 122 /** 123 * Scans a list of strings for a valid streamId. 124 * 125 * @param streamIdList list of stream id's to be scanned 126 * @return valid fingerprint or null if it can't be found 127 */ 128 public static Fingerprint extractFingerprint(List<String> streamIdList) { 129 for (String streamId : streamIdList) { 130 if (streamId.startsWith(STREAM_ID_CS_PREFIX)) { 131 return fromStreamId(streamId); 132 } 133 } 134 return null; 135 } 136 137 /** 138 * Encodes a 128-bit fingerprint as a string stream id. 139 * 140 * Stream id string is limited to 40 characters, which could be digits, lower case ASCII and 141 * underscores. 142 */ 143 public String toStreamId() { 144 StringBuilder streamId = new StringBuilder(STREAM_ID_CS_PREFIX); 145 appendHexFingerprint(streamId, mMd5Digest); 146 return streamId.toString(); 147 } 148 149 public byte[] getBytes() { 150 return mMd5Digest; 151 } 152 153 @Override 154 public boolean equals(Object obj) { 155 if (this == obj) return true; 156 if (!(obj instanceof Fingerprint)) return false; 157 Fingerprint other = (Fingerprint) obj; 158 return Arrays.equals(mMd5Digest, other.mMd5Digest); 159 } 160 161 public boolean equals(byte[] md5Digest) { 162 return Arrays.equals(mMd5Digest, md5Digest); 163 } 164 165 @Override 166 public int hashCode() { 167 return Arrays.hashCode(mMd5Digest); 168 } 169 170 // Utility methods. 171 172 private static int toDigit(String streamId, int index) { 173 int digit = Character.digit(streamId.charAt(index), 16); 174 if (digit < 0) { 175 throw new IllegalArgumentException("illegal hex digit in " + streamId); 176 } 177 return digit; 178 } 179 180 private static void appendHexFingerprint(StringBuilder sb, byte[] bytes) { 181 for (int idx = 0; idx < FINGERPRINT_BYTE_LENGTH; idx++) { 182 int value = bytes[idx]; 183 sb.append(Integer.toHexString((value >> 4) & 0x0f)); 184 sb.append(Integer.toHexString(value& 0x0f)); 185 } 186 } 187} 188