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