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