summaryrefslogtreecommitdiffstats
path: root/gallerycommon/src/com/android/gallery3d/common/BlobCache.java
blob: 7788e61f5cad0a3906e425fae2845567a0dd7cb0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// This is an on-disk cache which maps a 64-bits key to a byte array.
//
// It consists of three files: one index file and two data files. One of the
// data files is "active", and the other is "inactive". New entries are
// appended into the active region until it reaches the size limit. At that
// point the active file and the inactive file are swapped, and the new active
// file is truncated to empty (and the index for that file is also cleared).
// The index is a hash table with linear probing. When the load factor reaches
// 0.5, it does the same thing like when the size limit is reached.
//
// The index file format: (all numbers are stored in little-endian)
// [0]  Magic number: 0xB3273030
// [4]  MaxEntries: Max number of hash entries per region.
// [8]  MaxBytes: Max number of data bytes per region (including header).
// [12] ActiveRegion: The active growing region: 0 or 1.
// [16] ActiveEntries: The number of hash entries used in the active region.
// [20] ActiveBytes: The number of data bytes used in the active region.
// [24] Version number.
// [28] Checksum of [0..28).
// [32] Hash entries for region 0. The size is X = (12 * MaxEntries bytes).
// [32 + X] Hash entries for region 1. The size is also X.
//
// Each hash entry is 12 bytes: 8 bytes key and 4 bytes offset into the data
// file. The offset is 0 when the slot is free. Note that 0 is a valid value
// for key. The keys are used directly as index into a hash table, so they
// should be suitably distributed.
//
// Each data file stores data for one region. The data file is concatenated
// blobs followed by the magic number 0xBD248510.
//
// The blob format:
// [0]  Key of this blob
// [8]  Checksum of this blob
// [12] Offset of this blob
// [16] Length of this blob (not including header)
// [20] Blob
//
// Below are the interface for BlobCache. The instance of this class does not
// support concurrent use by multiple threads.
//
// public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) throws IOException;
// public void insert(long key, byte[] data) throws IOException;
// public byte[] lookup(long key) throws IOException;
// public void lookup(LookupRequest req) throws IOException;
// public void close();
// public void syncIndex();
// public void syncAll();
// public static void deleteFiles(String path);
//
package com.android.gallery3d.common;

import android.util.Log;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.zip.Adler32;

public class BlobCache implements Closeable {
    private static final String TAG = "BlobCache";

    private static final int MAGIC_INDEX_FILE = 0xB3273030;
    private static final int MAGIC_DATA_FILE = 0xBD248510;

    // index header offset
    private static final int IH_MAGIC = 0;
    private static final int IH_MAX_ENTRIES = 4;
    private static final int IH_MAX_BYTES = 8;
    private static final int IH_ACTIVE_REGION = 12;
    private static final int IH_ACTIVE_ENTRIES = 16;
    private static final int IH_ACTIVE_BYTES = 20;
    private static final int IH_VERSION = 24;
    private static final int IH_CHECKSUM = 28;
    private static final int INDEX_HEADER_SIZE = 32;

    private static final int DATA_HEADER_SIZE = 4;

    // blob header offset
    private static final int BH_KEY = 0;
    private static final int BH_CHECKSUM = 8;
    private static final int BH_OFFSET = 12;
    private static final int BH_LENGTH = 16;
    private static final int BLOB_HEADER_SIZE = 20;

    private RandomAccessFile mIndexFile;
    private RandomAccessFile mDataFile0;
    private RandomAccessFile mDataFile1;
    private FileChannel mIndexChannel;
    private MappedByteBuffer mIndexBuffer;

    private int mMaxEntries;
    private int mMaxBytes;
    private int mActiveRegion;
    private int mActiveEntries;
    private int mActiveBytes;
    private int mVersion;

    private RandomAccessFile mActiveDataFile;
    private RandomAccessFile mInactiveDataFile;
    private int mActiveHashStart;
    private int mInactiveHashStart;
    private byte[] mIndexHeader = new byte[INDEX_HEADER_SIZE];
    private byte[] mBlobHeader = new byte[BLOB_HEADER_SIZE];
    private Adler32 mAdler32 = new Adler32();

    // Creates the cache. Three files will be created:
    // path + ".idx", path + ".0", and path + ".1"
    // The ".0" file and the ".1" file each stores data for a region. Each of
    // them can grow to the size specified by maxBytes. The maxEntries parameter
    // specifies the maximum number of entries each region can have. If the
    // "reset" parameter is true, the cache will be cleared before use.
    public BlobCache(String path, int maxEntries, int maxBytes, boolean reset)
            throws IOException {
        this(path, maxEntries, maxBytes, reset, 0);
    }

    public BlobCache(String path, int maxEntries, int maxBytes, boolean reset,
            int version) throws IOException {
        mIndexFile = new RandomAccessFile(path + ".idx", "rw");
        mDataFile0 = new RandomAccessFile(path + ".0", "rw");
        mDataFile1 = new RandomAccessFile(path + ".1", "rw");
        mVersion = version;

        if (!reset && loadIndex()) {
            return;
        }

        resetCache(maxEntries, maxBytes);

        if (!loadIndex()) {
            closeAll();
            throw new IOException("unable to load index");
        }
    }

    // Delete the files associated with the given path previously created
    // by the BlobCache constructor.
    public static void deleteFiles(String path) {
        deleteFileSilently(path + ".idx");
        deleteFileSilently(path + ".0");
        deleteFileSilently(path + ".1");
    }

    private static void deleteFileSilently(String path) {
        try {
            new File(path).delete();
        } catch (Throwable t) {
            // ignore;
        }
    }

    // Close the cache. All resources are released. No other method should be
    // called after this is called.
    @Override
    public void close() {
        syncAll();
        closeAll();
    }

    private void closeAll() {
        closeSilently(mIndexChannel);
        closeSilently(mIndexFile);
        closeSilently(mDataFile0);
        closeSilently(mDataFile1);
    }

    // Returns true if loading index is successful. After this method is called,
    // mIndexHeader and index header in file should be kept sync.
    private boolean loadIndex() {
        try {
            mIndexFile.seek(0);
            mDataFile0.seek(0);
            mDataFile1.seek(0);

            byte[] buf = mIndexHeader;
            if (mIndexFile.read(buf) != INDEX_HEADER_SIZE) {
                Log.w(TAG, "cannot read header");
                return false;
            }

            if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) {
                Log.w(TAG, "cannot read header magic");
                return false;
            }

            if (readInt(buf, IH_VERSION) != mVersion) {
                Log.w(TAG, "version mismatch");
                return false;
            }

            mMaxEntries = readInt(buf, IH_MAX_ENTRIES);
            mMaxBytes = readInt(buf, IH_MAX_BYTES);
            mActiveRegion = readInt(buf, IH_ACTIVE_REGION);
            mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES);
            mActiveBytes = readInt(buf, IH_ACTIVE_BYTES);

            int sum = readInt(buf, IH_CHECKSUM);
            if (checkSum(buf, 0, IH_CHECKSUM) != sum) {
                Log.w(TAG, "header checksum does not match");
                return false;
            }

            // Sanity check
            if (mMaxEntries <= 0) {
                Log.w(TAG, "invalid max entries");
                return false;
            }
            if (mMaxBytes <= 0) {
                Log.w(TAG, "invalid max bytes");
                return false;
            }
            if (mActiveRegion != 0 && mActiveRegion != 1) {
                Log.w(TAG, "invalid active region");
                return false;
            }
            if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) {
                Log.w(TAG, "invalid active entries");
                return false;
            }
            if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) {
                Log.w(TAG, "invalid active bytes");
                return false;
            }
            if (mIndexFile.length() !=
                    INDEX_HEADER_SIZE + mMaxEntries * 12 * 2) {
                Log.w(TAG, "invalid index file length");
                return false;
            }

            // Make sure data file has magic
            byte[] magic = new byte[4];
            if (mDataFile0.read(magic) != 4) {
                Log.w(TAG, "cannot read data file magic");
                return false;
            }
            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
                Log.w(TAG, "invalid data file magic");
                return false;
            }
            if (mDataFile1.read(magic) != 4) {
                Log.w(TAG, "cannot read data file magic");
                return false;
            }
            if (readInt(magic, 0) != MAGIC_DATA_FILE) {
                Log.w(TAG, "invalid data file magic");
                return false;
            }

            // Map index file to memory
            mIndexChannel = mIndexFile.getChannel();
            mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE,
                    0, mIndexFile.length());
            mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN);

            setActiveVariables();
            return true;
        } catch (IOException ex) {
            Log.e(TAG, "loadIndex failed.", ex);
            return false;
        }
    }

    private void setActiveVariables() throws IOException {
        mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
        mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1;
        mActiveDataFile.setLength(mActiveBytes);
        mActiveDataFile.seek(mActiveBytes);

        mActiveHashStart = INDEX_HEADER_SIZE;
        mInactiveHashStart = INDEX_HEADER_SIZE;

        if (mActiveRegion == 0) {
            mInactiveHashStart += mMaxEntries * 12;
        } else {
            mActiveHashStart += mMaxEntries * 12;
        }
    }

    private void resetCache(int maxEntries, int maxBytes) throws IOException {
        mIndexFile.setLength(0);  // truncate to zero the index
        mIndexFile.setLength(INDEX_HEADER_SIZE + maxEntries * 12 * 2);
        mIndexFile.seek(0);
        byte[] buf = mIndexHeader;
        writeInt(buf, IH_MAGIC, MAGIC_INDEX_FILE);
        writeInt(buf, IH_MAX_ENTRIES, maxEntries);
        writeInt(buf, IH_MAX_BYTES, maxBytes);
        writeInt(buf, IH_ACTIVE_REGION, 0);
        writeInt(buf, IH_ACTIVE_ENTRIES, 0);
        writeInt(buf, IH_ACTIVE_BYTES, DATA_HEADER_SIZE);
        writeInt(buf, IH_VERSION, mVersion);
        writeInt(buf, IH_CHECKSUM, checkSum(buf, 0, IH_CHECKSUM));
        mIndexFile.write(buf);
        // This is only needed if setLength does not zero the extended part.
        // writeZero(mIndexFile, maxEntries * 12 * 2);

        mDataFile0.setLength(0);
        mDataFile1.setLength(0);
        mDataFile0.seek(0);
        mDataFile1.seek(0);
        writeInt(buf, 0, MAGIC_DATA_FILE);
        mDataFile0.write(buf, 0, 4);
        mDataFile1.write(buf, 0, 4);
    }

    // Flip the active region and the inactive region.
    private void flipRegion() throws IOException {
        mActiveRegion = 1 - mActiveRegion;
        mActiveEntries = 0;
        mActiveBytes = DATA_HEADER_SIZE;

        writeInt(mIndexHeader, IH_ACTIVE_REGION, mActiveRegion);
        writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
        writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
        updateIndexHeader();

        setActiveVariables();
        clearHash(mActiveHashStart);
        syncIndex();
    }

    // Sync mIndexHeader to the index file.
    private void updateIndexHeader() {
        writeInt(mIndexHeader, IH_CHECKSUM,
                checkSum(mIndexHeader, 0, IH_CHECKSUM));
        mIndexBuffer.position(0);
        mIndexBuffer.put(mIndexHeader);
    }

    // Clear the hash table starting from the specified offset.
    private void clearHash(int hashStart) {
        byte[] zero = new byte[1024];
        mIndexBuffer.position(hashStart);
        for (int count = mMaxEntries * 12; count > 0;) {
            int todo = Math.min(count, 1024);
            mIndexBuffer.put(zero, 0, todo);
            count -= todo;
        }
    }

    // Inserts a (key, data) pair into the cache.
    public void insert(long key, byte[] data) throws IOException {
        if (DATA_HEADER_SIZE + BLOB_HEADER_SIZE + data.length > mMaxBytes) {
            throw new RuntimeException("blob is too large!");
        }

        if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes
                || mActiveEntries * 2 >= mMaxEntries) {
            flipRegion();
        }

        if (!lookupInternal(key, mActiveHashStart)) {
            // If we don't have an existing entry with the same key, increase
            // the entry count.
            mActiveEntries++;
            writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
        }

        insertInternal(key, data, data.length);
        updateIndexHeader();
    }

    // Appends the data to the active file. It also updates the hash entry.
    // The proper hash entry (suitable for insertion or replacement) must be
    // pointed by mSlotOffset.
    private void insertInternal(long key, byte[] data, int length)
            throws IOException {
        byte[] header = mBlobHeader;
        int sum = checkSum(data);
        writeLong(header, BH_KEY, key);
        writeInt(header, BH_CHECKSUM, sum);
        writeInt(header, BH_OFFSET, mActiveBytes);
        writeInt(header, BH_LENGTH, length);
        mActiveDataFile.write(header);
        mActiveDataFile.write(data, 0, length);

        mIndexBuffer.putLong(mSlotOffset, key);
        mIndexBuffer.putInt(mSlotOffset + 8, mActiveBytes);
        mActiveBytes += BLOB_HEADER_SIZE + length;
        writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
    }

    public static class LookupRequest {
        public long key;        // input: the key to find
        public byte[] buffer;   // input/output: the buffer to store the blob
        public int length;      // output: the length of the blob
    }

    // This method is for one-off lookup. For repeated lookup, use the version
    // accepting LookupRequest to avoid repeated memory allocation.
    private LookupRequest mLookupRequest = new LookupRequest();
    public byte[] lookup(long key) throws IOException {
        mLookupRequest.key = key;
        mLookupRequest.buffer = null;
        if (lookup(mLookupRequest)) {
            return mLookupRequest.buffer;
        } else {
            return null;
        }
    }

    // Returns true if the associated blob for the given key is available.
    // The blob is stored in the buffer pointed by req.buffer, and the length
    // is in stored in the req.length variable.
    //
    // The user can input a non-null value in req.buffer, and this method will
    // try to use that buffer. If that buffer is not large enough, this method
    // will allocate a new buffer and assign it to req.buffer.
    //
    // This method tries not to throw IOException even if the data file is
    // corrupted, but it can still throw IOException if things get strange.
    public boolean lookup(LookupRequest req) throws IOException {
        // Look up in the active region first.
        if (lookupInternal(req.key, mActiveHashStart)) {
            if (getBlob(mActiveDataFile, mFileOffset, req)) {
                return true;
            }
        }

        // We want to copy the data from the inactive file to the active file
        // if it's available. So we keep the offset of the hash entry so we can
        // avoid looking it up again.
        int insertOffset = mSlotOffset;

        // Look up in the inactive region.
        if (lookupInternal(req.key, mInactiveHashStart)) {
            if (getBlob(mInactiveDataFile, mFileOffset, req)) {
                // If we don't have enough space to insert this blob into
                // the active file, just return it.
                if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes
                    || mActiveEntries * 2 >= mMaxEntries) {
                    return true;
                }
                // Otherwise copy it over.
                mSlotOffset = insertOffset;
                try {
                    insertInternal(req.key, req.buffer, req.length);
                    mActiveEntries++;
                    writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
                    updateIndexHeader();
                } catch (Throwable t) {
                    Log.e(TAG, "cannot copy over");
                }
                return true;
            }
        }

        return false;
    }


    // Copies the blob for the specified offset in the specified file to
    // req.buffer. If req.buffer is null or too small, allocate a buffer and
    // assign it to req.buffer.
    // Returns false if the blob is not available (either the index file is
    // not sync with the data file, or one of them is corrupted). The length
    // of the blob is stored in the req.length variable.
    private boolean getBlob(RandomAccessFile file, int offset,
            LookupRequest req) throws IOException {
        byte[] header = mBlobHeader;
        long oldPosition = file.getFilePointer();
        try {
            file.seek(offset);
            if (file.read(header) != BLOB_HEADER_SIZE) {
                Log.w(TAG, "cannot read blob header");
                return false;
            }
            long blobKey = readLong(header, BH_KEY);
            if (blobKey != req.key) {
                Log.w(TAG, "blob key does not match: " + blobKey);
                return false;
            }
            int sum = readInt(header, BH_CHECKSUM);
            int blobOffset = readInt(header, BH_OFFSET);
            if (blobOffset != offset) {
                Log.w(TAG, "blob offset does not match: " + blobOffset);
                return false;
            }
            int length = readInt(header, BH_LENGTH);
            if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) {
                Log.w(TAG, "invalid blob length: " + length);
                return false;
            }
            if (req.buffer == null || req.buffer.length < length) {
                req.buffer = new byte[length];
            }

            byte[] blob = req.buffer;
            req.length = length;

            if (file.read(blob, 0, length) != length) {
                Log.w(TAG, "cannot read blob data");
                return false;
            }
            if (checkSum(blob, 0, length) != sum) {
                Log.w(TAG, "blob checksum does not match: " + sum);
                return false;
            }
            return true;
        } catch (Throwable t)  {
            Log.e(TAG, "getBlob failed.", t);
            return false;
        } finally {
            file.seek(oldPosition);
        }
    }

    // Tries to look up a key in the specified hash region.
    // Returns true if the lookup is successful.
    // The slot offset in the index file is saved in mSlotOffset. If the lookup
    // is successful, it's the slot found. Otherwise it's the slot suitable for
    // insertion.
    // If the lookup is successful, the file offset is also saved in
    // mFileOffset.
    private int mSlotOffset;
    private int mFileOffset;
    private boolean lookupInternal(long key, int hashStart) {
        int slot = (int) (key % mMaxEntries);
        if (slot < 0) slot += mMaxEntries;
        int slotBegin = slot;
        while (true) {
            int offset = hashStart + slot * 12;
            long candidateKey = mIndexBuffer.getLong(offset);
            int candidateOffset = mIndexBuffer.getInt(offset + 8);
            if (candidateOffset == 0) {
                mSlotOffset = offset;
                return false;
            } else if (candidateKey == key) {
                mSlotOffset = offset;
                mFileOffset = candidateOffset;
                return true;
            } else {
                if (++slot >= mMaxEntries) {
                    slot = 0;
                }
                if (slot == slotBegin) {
                    Log.w(TAG, "corrupted index: clear the slot.");
                    mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0);
                }
            }
        }
    }

    public void syncIndex() {
        try {
            mIndexBuffer.force();
        } catch (Throwable t) {
            Log.w(TAG, "sync index failed", t);
        }
    }

    public void syncAll() {
        syncIndex();
        try {
            mDataFile0.getFD().sync();
        } catch (Throwable t) {
            Log.w(TAG, "sync data file 0 failed", t);
        }
        try {
            mDataFile1.getFD().sync();
        } catch (Throwable t) {
            Log.w(TAG, "sync data file 1 failed", t);
        }
    }

    // This is for testing only.
    //
    // Returns the active count (mActiveEntries). This also verifies that
    // the active count matches matches what's inside the hash region.
    int getActiveCount() {
        int count = 0;
        for (int i = 0; i < mMaxEntries; i++) {
            int offset = mActiveHashStart + i * 12;
            long candidateKey = mIndexBuffer.getLong(offset);
            int candidateOffset = mIndexBuffer.getInt(offset + 8);
            if (candidateOffset != 0) ++count;
        }
        if (count == mActiveEntries) {
            return count;
        } else {
            Log.e(TAG, "wrong active count: " + mActiveEntries + " vs " + count);
            return -1;  // signal failure.
        }
    }

    int checkSum(byte[] data) {
        mAdler32.reset();
        mAdler32.update(data);
        return (int) mAdler32.getValue();
    }

    int checkSum(byte[] data, int offset, int nbytes) {
        mAdler32.reset();
        mAdler32.update(data, offset, nbytes);
        return (int) mAdler32.getValue();
    }

    static void closeSilently(Closeable c) {
        if (c == null) return;
        try {
            c.close();
        } catch (Throwable t) {
            // do nothing
        }
    }

    static int readInt(byte[] buf, int offset) {
        return (buf[offset] & 0xff)
                | ((buf[offset + 1] & 0xff) << 8)
                | ((buf[offset + 2] & 0xff) << 16)
                | ((buf[offset + 3] & 0xff) << 24);
    }

    static long readLong(byte[] buf, int offset) {
        long result = buf[offset + 7] & 0xff;
        for (int i = 6; i >= 0; i--) {
            result = (result << 8) | (buf[offset + i] & 0xff);
        }
        return result;
    }

    static void writeInt(byte[] buf, int offset, int value) {
        for (int i = 0; i < 4; i++) {
            buf[offset + i] = (byte) (value & 0xff);
            value >>= 8;
        }
    }

    static void writeLong(byte[] buf, int offset, long value) {
        for (int i = 0; i < 8; i++) {
            buf[offset + i] = (byte) (value & 0xff);
            value >>= 8;
        }
    }
}