Home | History | Annotate | Download | only in common
      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      */
     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