1/*
2 * Copyright (C) 2010 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// This is an on-disk cache which maps a 64-bits key to a byte array.
18//
19// It consists of three files: one index file and two data files. One of the
20// data files is "active", and the other is "inactive". New entries are
21// appended into the active region until it reaches the size limit. At that
22// point the active file and the inactive file are swapped, and the new active
23// file is truncated to empty (and the index for that file is also cleared).
24// The index is a hash table with linear probing. When the load factor reaches
25// 0.5, it does the same thing like when the size limit is reached.
26//
27// The index file format: (all numbers are stored in little-endian)
28// [0]  Magic number: 0xB3273030
29// [4]  MaxEntries: Max number of hash entries per region.
30// [8]  MaxBytes: Max number of data bytes per region (including header).
31// [12] ActiveRegion: The active growing region: 0 or 1.
32// [16] ActiveEntries: The number of hash entries used in the active region.
33// [20] ActiveBytes: The number of data bytes used in the active region.
34// [24] Version number.
35// [28] Checksum of [0..28).
36// [32] Hash entries for region 0. The size is X = (12 * MaxEntries bytes).
37// [32 + X] Hash entries for region 1. The size is also X.
38//
39// Each hash entry is 12 bytes: 8 bytes key and 4 bytes offset into the data
40// file. The offset is 0 when the slot is free. Note that 0 is a valid value
41// for key. The keys are used directly as index into a hash table, so they
42// should be suitably distributed.
43//
44// Each data file stores data for one region. The data file is concatenated
45// blobs followed by the magic number 0xBD248510.
46//
47// The blob format:
48// [0]  Key of this blob
49// [8]  Checksum of this blob
50// [12] Offset of this blob
51// [16] Length of this blob (not including header)
52// [20] Blob
53//
54// Below are the interface for BlobCache. The instance of this class does not
55// support concurrent use by multiple threads.
56//
57// public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) throws IOException;
58// public void insert(long key, byte[] data) throws IOException;
59// public byte[] lookup(long key) throws IOException;
60// public void lookup(LookupRequest req) throws IOException;
61// public void close();
62// public void syncIndex();
63// public void syncAll();
64// public static void deleteFiles(String path);
65//
66package com.android.gallery3d.common;
67
68import android.util.Log;
69
70import java.io.Closeable;
71import java.io.File;
72import java.io.IOException;
73import java.io.RandomAccessFile;
74import java.nio.ByteOrder;
75import java.nio.MappedByteBuffer;
76import java.nio.channels.FileChannel;
77import java.util.Arrays;
78import java.util.zip.Adler32;
79
80public class BlobCache implements Closeable {
81    private static final String TAG = "BlobCache";
82
83    private static final int MAGIC_INDEX_FILE = 0xB3273030;
84    private static final int MAGIC_DATA_FILE = 0xBD248510;
85
86    // index header offset
87    private static final int IH_MAGIC = 0;
88    private static final int IH_MAX_ENTRIES = 4;
89    private static final int IH_MAX_BYTES = 8;
90    private static final int IH_ACTIVE_REGION = 12;
91    private static final int IH_ACTIVE_ENTRIES = 16;
92    private static final int IH_ACTIVE_BYTES = 20;
93    private static final int IH_VERSION = 24;
94    private static final int IH_CHECKSUM = 28;
95    private static final int INDEX_HEADER_SIZE = 32;
96
97    private static final int DATA_HEADER_SIZE = 4;
98
99    // blob header offset
100    private static final int BH_KEY = 0;
101    private static final int BH_CHECKSUM = 8;
102    private static final int BH_OFFSET = 12;
103    private static final int BH_LENGTH = 16;
104    private static final int BLOB_HEADER_SIZE = 20;
105
106    private RandomAccessFile mIndexFile;
107    private RandomAccessFile mDataFile0;
108    private RandomAccessFile mDataFile1;
109    private FileChannel mIndexChannel;
110    private MappedByteBuffer mIndexBuffer;
111
112    private int mMaxEntries;
113    private int mMaxBytes;
114    private int mActiveRegion;
115    private int mActiveEntries;
116    private int mActiveBytes;
117    private int mVersion;
118
119    private RandomAccessFile mActiveDataFile;
120    private RandomAccessFile mInactiveDataFile;
121    private int mActiveHashStart;
122    private int mInactiveHashStart;
123    private byte[] mIndexHeader = new byte[INDEX_HEADER_SIZE];
124    private byte[] mBlobHeader = new byte[BLOB_HEADER_SIZE];
125    private Adler32 mAdler32 = new Adler32();
126
127    // Creates the cache. Three files will be created:
128    // path + ".idx", path + ".0", and path + ".1"
129    // The ".0" file and the ".1" file each stores data for a region. Each of
130    // them can grow to the size specified by maxBytes. The maxEntries parameter
131    // specifies the maximum number of entries each region can have. If the
132    // "reset" parameter is true, the cache will be cleared before use.
133    public BlobCache(String path, int maxEntries, int maxBytes, boolean reset)
134            throws IOException {
135        this(path, maxEntries, maxBytes, reset, 0);
136    }
137
138    public BlobCache(String path, int maxEntries, int maxBytes, boolean reset,
139            int version) throws IOException {
140        mIndexFile = new RandomAccessFile(path + ".idx", "rw");
141        mDataFile0 = new RandomAccessFile(path + ".0", "rw");
142        mDataFile1 = new RandomAccessFile(path + ".1", "rw");
143        mVersion = version;
144
145        if (!reset && loadIndex()) {
146            return;
147        }
148
149        resetCache(maxEntries, maxBytes);
150
151        if (!loadIndex()) {
152            closeAll();
153            throw new IOException("unable to load index");
154        }
155    }
156
157    // Delete the files associated with the given path previously created
158    // by the BlobCache constructor.
159    public static void deleteFiles(String path) {
160        deleteFileSilently(path + ".idx");
161        deleteFileSilently(path + ".0");
162        deleteFileSilently(path + ".1");
163    }
164
165    private static void deleteFileSilently(String path) {
166        try {
167            new File(path).delete();
168        } catch (Throwable t) {
169            // ignore;
170        }
171    }
172
173    // Close the cache. All resources are released. No other method should be
174    // called after this is called.
175    @Override
176    public void close() {
177        syncAll();
178        closeAll();
179    }
180
181    private void closeAll() {
182        closeSilently(mIndexChannel);
183        closeSilently(mIndexFile);
184        closeSilently(mDataFile0);
185        closeSilently(mDataFile1);
186    }
187
188    // Returns true if loading index is successful. After this method is called,
189    // mIndexHeader and index header in file should be kept sync.
190    private boolean loadIndex() {
191        try {
192            mIndexFile.seek(0);
193            mDataFile0.seek(0);
194            mDataFile1.seek(0);
195
196            byte[] buf = mIndexHeader;
197            if (mIndexFile.read(buf) != INDEX_HEADER_SIZE) {
198                Log.w(TAG, "cannot read header");
199                return false;
200            }
201
202            if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) {
203                Log.w(TAG, "cannot read header magic");
204                return false;
205            }
206
207            if (readInt(buf, IH_VERSION) != mVersion) {
208                Log.w(TAG, "version mismatch");
209                return false;
210            }
211
212            mMaxEntries = readInt(buf, IH_MAX_ENTRIES);
213            mMaxBytes = readInt(buf, IH_MAX_BYTES);
214            mActiveRegion = readInt(buf, IH_ACTIVE_REGION);
215            mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES);
216            mActiveBytes = readInt(buf, IH_ACTIVE_BYTES);
217
218            int sum = readInt(buf, IH_CHECKSUM);
219            if (checkSum(buf, 0, IH_CHECKSUM) != sum) {
220                Log.w(TAG, "header checksum does not match");
221                return false;
222            }
223
224            // Sanity check
225            if (mMaxEntries <= 0) {
226                Log.w(TAG, "invalid max entries");
227                return false;
228            }
229            if (mMaxBytes <= 0) {
230                Log.w(TAG, "invalid max bytes");
231                return false;
232            }
233            if (mActiveRegion != 0 && mActiveRegion != 1) {
234                Log.w(TAG, "invalid active region");
235                return false;
236            }
237            if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) {
238                Log.w(TAG, "invalid active entries");
239                return false;
240            }
241            if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) {
242                Log.w(TAG, "invalid active bytes");
243                return false;
244            }
245            if (mIndexFile.length() !=
246                    INDEX_HEADER_SIZE + mMaxEntries * 12 * 2) {
247                Log.w(TAG, "invalid index file length");
248                return false;
249            }
250
251            // Make sure data file has magic
252            byte[] magic = new byte[4];
253            if (mDataFile0.read(magic) != 4) {
254                Log.w(TAG, "cannot read data file magic");
255                return false;
256            }
257            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
258                Log.w(TAG, "invalid data file magic");
259                return false;
260            }
261            if (mDataFile1.read(magic) != 4) {
262                Log.w(TAG, "cannot read data file magic");
263                return false;
264            }
265            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
266                Log.w(TAG, "invalid data file magic");
267                return false;
268            }
269
270            // Map index file to memory
271            mIndexChannel = mIndexFile.getChannel();
272            mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE,
273                    0, mIndexFile.length());
274            mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN);
275
276            setActiveVariables();
277            return true;
278        } catch (IOException ex) {
279            Log.e(TAG, "loadIndex failed.", ex);
280            return false;
281        }
282    }
283
284    private void setActiveVariables() throws IOException {
285        mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
286        mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1;
287        mActiveDataFile.setLength(mActiveBytes);
288        mActiveDataFile.seek(mActiveBytes);
289
290        mActiveHashStart = INDEX_HEADER_SIZE;
291        mInactiveHashStart = INDEX_HEADER_SIZE;
292
293        if (mActiveRegion == 0) {
294            mInactiveHashStart += mMaxEntries * 12;
295        } else {
296            mActiveHashStart += mMaxEntries * 12;
297        }
298    }
299
300    private void resetCache(int maxEntries, int maxBytes) throws IOException {
301        mIndexFile.setLength(0);  // truncate to zero the index
302        mIndexFile.setLength(INDEX_HEADER_SIZE + maxEntries * 12 * 2);
303        mIndexFile.seek(0);
304        byte[] buf = mIndexHeader;
305        writeInt(buf, IH_MAGIC, MAGIC_INDEX_FILE);
306        writeInt(buf, IH_MAX_ENTRIES, maxEntries);
307        writeInt(buf, IH_MAX_BYTES, maxBytes);
308        writeInt(buf, IH_ACTIVE_REGION, 0);
309        writeInt(buf, IH_ACTIVE_ENTRIES, 0);
310        writeInt(buf, IH_ACTIVE_BYTES, DATA_HEADER_SIZE);
311        writeInt(buf, IH_VERSION, mVersion);
312        writeInt(buf, IH_CHECKSUM, checkSum(buf, 0, IH_CHECKSUM));
313        mIndexFile.write(buf);
314        // This is only needed if setLength does not zero the extended part.
315        // writeZero(mIndexFile, maxEntries * 12 * 2);
316
317        mDataFile0.setLength(0);
318        mDataFile1.setLength(0);
319        mDataFile0.seek(0);
320        mDataFile1.seek(0);
321        writeInt(buf, 0, MAGIC_DATA_FILE);
322        mDataFile0.write(buf, 0, 4);
323        mDataFile1.write(buf, 0, 4);
324    }
325
326    // Flip the active region and the inactive region.
327    private void flipRegion() throws IOException {
328        mActiveRegion = 1 - mActiveRegion;
329        mActiveEntries = 0;
330        mActiveBytes = DATA_HEADER_SIZE;
331
332        writeInt(mIndexHeader, IH_ACTIVE_REGION, mActiveRegion);
333        writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
334        writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
335        updateIndexHeader();
336
337        setActiveVariables();
338        clearHash(mActiveHashStart);
339        syncIndex();
340    }
341
342    // Sync mIndexHeader to the index file.
343    private void updateIndexHeader() {
344        writeInt(mIndexHeader, IH_CHECKSUM,
345                checkSum(mIndexHeader, 0, IH_CHECKSUM));
346        mIndexBuffer.position(0);
347        mIndexBuffer.put(mIndexHeader);
348    }
349
350    // Clear the hash table starting from the specified offset.
351    private void clearHash(int hashStart) {
352        byte[] zero = new byte[1024];
353        mIndexBuffer.position(hashStart);
354        for (int count = mMaxEntries * 12; count > 0;) {
355            int todo = Math.min(count, 1024);
356            mIndexBuffer.put(zero, 0, todo);
357            count -= todo;
358        }
359    }
360
361    // Inserts a (key, data) pair into the cache.
362    public void insert(long key, byte[] data) throws IOException {
363        if (DATA_HEADER_SIZE + BLOB_HEADER_SIZE + data.length > mMaxBytes) {
364            throw new RuntimeException("blob is too large!");
365        }
366
367        if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes
368                || mActiveEntries * 2 >= mMaxEntries) {
369            flipRegion();
370        }
371
372        if (!lookupInternal(key, mActiveHashStart)) {
373            // If we don't have an existing entry with the same key, increase
374            // the entry count.
375            mActiveEntries++;
376            writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
377        }
378
379        insertInternal(key, data, data.length);
380        updateIndexHeader();
381    }
382
383    public void clearEntry(long key) throws IOException {
384        if (!lookupInternal(key, mActiveHashStart)) {
385            return; // Nothing to clear
386        }
387        byte[] header = mBlobHeader;
388        Arrays.fill(header, (byte) 0);
389        mActiveDataFile.seek(mFileOffset);
390        mActiveDataFile.write(header);
391    }
392
393    // Appends the data to the active file. It also updates the hash entry.
394    // The proper hash entry (suitable for insertion or replacement) must be
395    // pointed by mSlotOffset.
396    private void insertInternal(long key, byte[] data, int length)
397            throws IOException {
398        byte[] header = mBlobHeader;
399        int sum = checkSum(data);
400        writeLong(header, BH_KEY, key);
401        writeInt(header, BH_CHECKSUM, sum);
402        writeInt(header, BH_OFFSET, mActiveBytes);
403        writeInt(header, BH_LENGTH, length);
404        mActiveDataFile.write(header);
405        mActiveDataFile.write(data, 0, length);
406
407        mIndexBuffer.putLong(mSlotOffset, key);
408        mIndexBuffer.putInt(mSlotOffset + 8, mActiveBytes);
409        mActiveBytes += BLOB_HEADER_SIZE + length;
410        writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
411    }
412
413    public static class LookupRequest {
414        public long key;        // input: the key to find
415        public byte[] buffer;   // input/output: the buffer to store the blob
416        public int length;      // output: the length of the blob
417    }
418
419    // This method is for one-off lookup. For repeated lookup, use the version
420    // accepting LookupRequest to avoid repeated memory allocation.
421    private LookupRequest mLookupRequest = new LookupRequest();
422    public byte[] lookup(long key) throws IOException {
423        mLookupRequest.key = key;
424        mLookupRequest.buffer = null;
425        if (lookup(mLookupRequest)) {
426            return mLookupRequest.buffer;
427        } else {
428            return null;
429        }
430    }
431
432    // Returns true if the associated blob for the given key is available.
433    // The blob is stored in the buffer pointed by req.buffer, and the length
434    // is in stored in the req.length variable.
435    //
436    // The user can input a non-null value in req.buffer, and this method will
437    // try to use that buffer. If that buffer is not large enough, this method
438    // will allocate a new buffer and assign it to req.buffer.
439    //
440    // This method tries not to throw IOException even if the data file is
441    // corrupted, but it can still throw IOException if things get strange.
442    public boolean lookup(LookupRequest req) throws IOException {
443        // Look up in the active region first.
444        if (lookupInternal(req.key, mActiveHashStart)) {
445            if (getBlob(mActiveDataFile, mFileOffset, req)) {
446                return true;
447            }
448        }
449
450        // We want to copy the data from the inactive file to the active file
451        // if it's available. So we keep the offset of the hash entry so we can
452        // avoid looking it up again.
453        int insertOffset = mSlotOffset;
454
455        // Look up in the inactive region.
456        if (lookupInternal(req.key, mInactiveHashStart)) {
457            if (getBlob(mInactiveDataFile, mFileOffset, req)) {
458                // If we don't have enough space to insert this blob into
459                // the active file, just return it.
460                if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes
461                    || mActiveEntries * 2 >= mMaxEntries) {
462                    return true;
463                }
464                // Otherwise copy it over.
465                mSlotOffset = insertOffset;
466                try {
467                    insertInternal(req.key, req.buffer, req.length);
468                    mActiveEntries++;
469                    writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
470                    updateIndexHeader();
471                } catch (Throwable t) {
472                    Log.e(TAG, "cannot copy over");
473                }
474                return true;
475            }
476        }
477
478        return false;
479    }
480
481
482    // Copies the blob for the specified offset in the specified file to
483    // req.buffer. If req.buffer is null or too small, allocate a buffer and
484    // assign it to req.buffer.
485    // Returns false if the blob is not available (either the index file is
486    // not sync with the data file, or one of them is corrupted). The length
487    // of the blob is stored in the req.length variable.
488    private boolean getBlob(RandomAccessFile file, int offset,
489            LookupRequest req) throws IOException {
490        byte[] header = mBlobHeader;
491        long oldPosition = file.getFilePointer();
492        try {
493            file.seek(offset);
494            if (file.read(header) != BLOB_HEADER_SIZE) {
495                Log.w(TAG, "cannot read blob header");
496                return false;
497            }
498            long blobKey = readLong(header, BH_KEY);
499            if (blobKey == 0) {
500                return false; // This entry has been cleared.
501            }
502            if (blobKey != req.key) {
503                Log.w(TAG, "blob key does not match: " + blobKey);
504                return false;
505            }
506            int sum = readInt(header, BH_CHECKSUM);
507            int blobOffset = readInt(header, BH_OFFSET);
508            if (blobOffset != offset) {
509                Log.w(TAG, "blob offset does not match: " + blobOffset);
510                return false;
511            }
512            int length = readInt(header, BH_LENGTH);
513            if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) {
514                Log.w(TAG, "invalid blob length: " + length);
515                return false;
516            }
517            if (req.buffer == null || req.buffer.length < length) {
518                req.buffer = new byte[length];
519            }
520
521            byte[] blob = req.buffer;
522            req.length = length;
523
524            if (file.read(blob, 0, length) != length) {
525                Log.w(TAG, "cannot read blob data");
526                return false;
527            }
528            if (checkSum(blob, 0, length) != sum) {
529                Log.w(TAG, "blob checksum does not match: " + sum);
530                return false;
531            }
532            return true;
533        } catch (Throwable t)  {
534            Log.e(TAG, "getBlob failed.", t);
535            return false;
536        } finally {
537            file.seek(oldPosition);
538        }
539    }
540
541    // Tries to look up a key in the specified hash region.
542    // Returns true if the lookup is successful.
543    // The slot offset in the index file is saved in mSlotOffset. If the lookup
544    // is successful, it's the slot found. Otherwise it's the slot suitable for
545    // insertion.
546    // If the lookup is successful, the file offset is also saved in
547    // mFileOffset.
548    private int mSlotOffset;
549    private int mFileOffset;
550    private boolean lookupInternal(long key, int hashStart) {
551        int slot = (int) (key % mMaxEntries);
552        if (slot < 0) slot += mMaxEntries;
553        int slotBegin = slot;
554        while (true) {
555            int offset = hashStart + slot * 12;
556            long candidateKey = mIndexBuffer.getLong(offset);
557            int candidateOffset = mIndexBuffer.getInt(offset + 8);
558            if (candidateOffset == 0) {
559                mSlotOffset = offset;
560                return false;
561            } else if (candidateKey == key) {
562                mSlotOffset = offset;
563                mFileOffset = candidateOffset;
564                return true;
565            } else {
566                if (++slot >= mMaxEntries) {
567                    slot = 0;
568                }
569                if (slot == slotBegin) {
570                    Log.w(TAG, "corrupted index: clear the slot.");
571                    mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0);
572                }
573            }
574        }
575    }
576
577    public void syncIndex() {
578        try {
579            mIndexBuffer.force();
580        } catch (Throwable t) {
581            Log.w(TAG, "sync index failed", t);
582        }
583    }
584
585    public void syncAll() {
586        syncIndex();
587        try {
588            mDataFile0.getFD().sync();
589        } catch (Throwable t) {
590            Log.w(TAG, "sync data file 0 failed", t);
591        }
592        try {
593            mDataFile1.getFD().sync();
594        } catch (Throwable t) {
595            Log.w(TAG, "sync data file 1 failed", t);
596        }
597    }
598
599    // This is for testing only.
600    //
601    // Returns the active count (mActiveEntries). This also verifies that
602    // the active count matches matches what's inside the hash region.
603    int getActiveCount() {
604        int count = 0;
605        for (int i = 0; i < mMaxEntries; i++) {
606            int offset = mActiveHashStart + i * 12;
607            long candidateKey = mIndexBuffer.getLong(offset);
608            int candidateOffset = mIndexBuffer.getInt(offset + 8);
609            if (candidateOffset != 0) ++count;
610        }
611        if (count == mActiveEntries) {
612            return count;
613        } else {
614            Log.e(TAG, "wrong active count: " + mActiveEntries + " vs " + count);
615            return -1;  // signal failure.
616        }
617    }
618
619    int checkSum(byte[] data) {
620        mAdler32.reset();
621        mAdler32.update(data);
622        return (int) mAdler32.getValue();
623    }
624
625    int checkSum(byte[] data, int offset, int nbytes) {
626        mAdler32.reset();
627        mAdler32.update(data, offset, nbytes);
628        return (int) mAdler32.getValue();
629    }
630
631    static void closeSilently(Closeable c) {
632        if (c == null) return;
633        try {
634            c.close();
635        } catch (Throwable t) {
636            // do nothing
637        }
638    }
639
640    static int readInt(byte[] buf, int offset) {
641        return (buf[offset] & 0xff)
642                | ((buf[offset + 1] & 0xff) << 8)
643                | ((buf[offset + 2] & 0xff) << 16)
644                | ((buf[offset + 3] & 0xff) << 24);
645    }
646
647    static long readLong(byte[] buf, int offset) {
648        long result = buf[offset + 7] & 0xff;
649        for (int i = 6; i >= 0; i--) {
650            result = (result << 8) | (buf[offset + i] & 0xff);
651        }
652        return result;
653    }
654
655    static void writeInt(byte[] buf, int offset, int value) {
656        for (int i = 0; i < 4; i++) {
657            buf[offset + i] = (byte) (value & 0xff);
658            value >>= 8;
659        }
660    }
661
662    static void writeLong(byte[] buf, int offset, long value) {
663        for (int i = 0; i < 8; i++) {
664            buf[offset + i] = (byte) (value & 0xff);
665            value >>= 8;
666        }
667    }
668}
669