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 
17 package com.android.gallery3d.common;
18 
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.security.DigestInputStream;
22 import java.security.MessageDigest;
23 import java.security.NoSuchAlgorithmException;
24 import java.util.Arrays;
25 import java.util.List;
26 
27 /**
28  * MD5-based digest Wrapper.
29  */
30 public 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      */
Fingerprint(byte[] bytes)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      */
fromInputStream(InputStream stream, long[] byteCount)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      */
fromStreamId(String streamId)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      */
extractFingerprint(List<String> streamIdList)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      */
toStreamId()143     public String toStreamId() {
144         StringBuilder streamId = new StringBuilder(STREAM_ID_CS_PREFIX);
145         appendHexFingerprint(streamId, mMd5Digest);
146         return streamId.toString();
147     }
148 
getBytes()149     public byte[] getBytes() {
150         return mMd5Digest;
151     }
152 
153     @Override
equals(Object obj)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 
equals(byte[] md5Digest)161     public boolean equals(byte[] md5Digest) {
162         return Arrays.equals(mMd5Digest, md5Digest);
163     }
164 
165     @Override
hashCode()166     public int hashCode() {
167         return Arrays.hashCode(mMd5Digest);
168     }
169 
170     // Utility methods.
171 
toDigit(String streamId, int index)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 
appendHexFingerprint(StringBuilder sb, byte[] bytes)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