• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
BlobCache(String path, int maxEntries, int maxBytes, boolean reset)132     public BlobCache(String path, int maxEntries, int maxBytes, boolean reset)
133             throws IOException {
134         this(path, maxEntries, maxBytes, reset, 0);
135     }
136 
BlobCache(String path, int maxEntries, int maxBytes, boolean reset, int version)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.
deleteFiles(String path)158     public static void deleteFiles(String path) {
159         deleteFileSilently(path + ".idx");
160         deleteFileSilently(path + ".0");
161         deleteFileSilently(path + ".1");
162     }
163 
deleteFileSilently(String path)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
close()175     public void close() {
176         syncAll();
177         closeAll();
178     }
179 
closeAll()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.
loadIndex()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 
setActiveVariables()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 
resetCache(int maxEntries, int maxBytes)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.
flipRegion()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.
updateIndexHeader()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.
clearHash(int hashStart)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.
insert(long key, byte[] data)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.
insertInternal(long key, byte[] data, int length)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();
lookup(long key)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.
lookup(LookupRequest req)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.
getBlob(RandomAccessFile file, int offset, LookupRequest req)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;
lookupInternal(long key, int hashStart)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 
syncIndex()563     public void syncIndex() {
564         try {
565             mIndexBuffer.force();
566         } catch (Throwable t) {
567             Log.w(TAG, "sync index failed", t);
568         }
569     }
570 
syncAll()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.
getActiveCount()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 
checkSum(byte[] data)605     int checkSum(byte[] data) {
606         mAdler32.reset();
607         mAdler32.update(data);
608         return (int) mAdler32.getValue();
609     }
610 
checkSum(byte[] data, int offset, int nbytes)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 
closeSilently(Closeable c)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 
readInt(byte[] buf, int offset)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 
readLong(byte[] buf, int offset)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 
writeInt(byte[] buf, int offset, int value)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 
writeLong(byte[] buf, int offset, long value)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