View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://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, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.compression;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.util.concurrent.FastThreadLocal;
20  import io.netty.util.internal.MathUtil;
21  import io.netty.util.internal.SystemPropertyUtil;
22  
23  import java.util.Arrays;
24  
25  /**
26   * Uncompresses an input {@link ByteBuf} encoded with Snappy compression into an
27   * output {@link ByteBuf}.
28   *
29   * See <a href="https://github.com/google/snappy/blob/master/format_description.txt">snappy format</a>.
30   */
31  public final class Snappy {
32  
33      private static final int MAX_HT_SIZE = 1 << 14;
34      private static final int MIN_COMPRESSIBLE_BYTES = 15;
35  
36      // used as a return value to indicate that we haven't yet read our full preamble
37      private static final int PREAMBLE_NOT_FULL = -1;
38      private static final int NOT_ENOUGH_INPUT = -1;
39  
40      // constants for the tag types
41      private static final int LITERAL = 0;
42      private static final int COPY_1_BYTE_OFFSET = 1;
43      private static final int COPY_2_BYTE_OFFSET = 2;
44      private static final int COPY_4_BYTE_OFFSET = 3;
45  
46      // Hash table used to compress, shared between subsequent call to .encode()
47      private static final FastThreadLocal<short[]> HASH_TABLE = new FastThreadLocal<short[]>();
48  
49      private static final boolean DEFAULT_REUSE_HASHTABLE =
50              SystemPropertyUtil.getBoolean("io.netty.handler.codec.compression.snappy.reuseHashTable", false);
51  
52      public Snappy() {
53          this(DEFAULT_REUSE_HASHTABLE);
54      }
55  
56      Snappy(boolean reuseHashtable) {
57          this.reuseHashtable = reuseHashtable;
58      }
59  
60      public static Snappy withHashTableReuse() {
61          return new Snappy(true);
62      }
63  
64      private final boolean reuseHashtable;
65      private State state = State.READING_PREAMBLE;
66      private byte tag;
67      private int written;
68  
69      private enum State {
70          READING_PREAMBLE,
71          READING_TAG,
72          READING_LITERAL,
73          READING_COPY
74      }
75  
76      public void reset() {
77          state = State.READING_PREAMBLE;
78          tag = 0;
79          written = 0;
80      }
81  
82      public void encode(final ByteBuf in, final ByteBuf out, final int length) {
83          // Write the preamble length to the output buffer
84          for (int i = 0;; i ++) {
85              int b = length >>> i * 7;
86              if ((b & 0xFFFFFF80) != 0) {
87                  out.writeByte(b & 0x7f | 0x80);
88              } else {
89                  out.writeByte(b);
90                  break;
91              }
92          }
93  
94          int inIndex = in.readerIndex();
95          final int baseIndex = inIndex;
96  
97          int hashTableSize = MathUtil.findNextPositivePowerOfTwo(length);
98          hashTableSize = Math.min(hashTableSize, MAX_HT_SIZE);
99          final short[] table = getHashTable(hashTableSize);
100         final int shift = Integer.numberOfLeadingZeros(hashTableSize) + 1;
101 
102         int nextEmit = inIndex;
103 
104         if (length - inIndex >= MIN_COMPRESSIBLE_BYTES) {
105             int nextHash = hash(in, ++inIndex, shift);
106             outer: while (true) {
107                 int skip = 32;
108 
109                 int candidate;
110                 int nextIndex = inIndex;
111                 do {
112                     inIndex = nextIndex;
113                     int hash = nextHash;
114                     int bytesBetweenHashLookups = skip++ >> 5;
115                     nextIndex = inIndex + bytesBetweenHashLookups;
116 
117                     // We need at least 4 remaining bytes to read the hash
118                     if (nextIndex > length - 4) {
119                         break outer;
120                     }
121 
122                     nextHash = hash(in, nextIndex, shift);
123 
124                     // equivalent to Short.toUnsignedInt
125                     // use unsigned short cast to avoid loss precision when 32767 <= length <= 65355
126                     candidate = baseIndex + ((int) table[hash]) & 0xffff;
127 
128                     table[hash] = (short) (inIndex - baseIndex);
129                 }
130                 while (in.getInt(inIndex) != in.getInt(candidate));
131 
132                 encodeLiteral(in, out, inIndex - nextEmit);
133 
134                 int insertTail;
135                 do {
136                     int base = inIndex;
137                     int matched = 4 + findMatchingLength(in, candidate + 4, inIndex + 4, length);
138                     inIndex += matched;
139                     int offset = base - candidate;
140                     encodeCopy(out, offset, matched);
141                     in.readerIndex(in.readerIndex() + matched);
142                     insertTail = inIndex - 1;
143                     nextEmit = inIndex;
144                     if (inIndex >= length - 4) {
145                         break outer;
146                     }
147 
148                     int prevHash = hash(in, insertTail, shift);
149                     table[prevHash] = (short) (inIndex - baseIndex - 1);
150                     int currentHash = hash(in, insertTail + 1, shift);
151                     candidate = baseIndex + table[currentHash];
152                     table[currentHash] = (short) (inIndex - baseIndex);
153                 }
154                 while (in.getInt(insertTail + 1) == in.getInt(candidate));
155 
156                 nextHash = hash(in, insertTail + 2, shift);
157                 ++inIndex;
158             }
159         }
160 
161         // If there are any remaining characters, write them out as a literal
162         if (nextEmit < length) {
163             encodeLiteral(in, out, length - nextEmit);
164         }
165     }
166 
167     /**
168      * Hashes the 4 bytes located at index, shifting the resulting hash into
169      * the appropriate range for our hash table.
170      *
171      * @param in The input buffer to read 4 bytes from
172      * @param index The index to read at
173      * @param shift The shift value, for ensuring that the resulting value is
174      *     within the range of our hash table size
175      * @return A 32-bit hash of 4 bytes located at index
176      */
177     private static int hash(ByteBuf in, int index, int shift) {
178         return in.getInt(index) * 0x1e35a7bd >>> shift;
179     }
180 
181     /**
182      * Returns a short[] to be used as a hashtable
183      *
184      * @param hashTableSize the size for the hashtable
185      * @return An appropriately sized empty hashtable
186      */
187     private short[] getHashTable(int hashTableSize) {
188         if (reuseHashtable) {
189             return getHashTableFastThreadLocalArrayFill(hashTableSize);
190         }
191         return new short[hashTableSize];
192     }
193 
194     /**
195      * Returns a short[] from a FastThreadLocal, zeroing for correctness
196      * creating a new one and resizing it if necessary
197      *
198      * @return An appropriately sized empty hashtable
199      * @param hashTableSize
200      */
201     public static short[] getHashTableFastThreadLocalArrayFill(int hashTableSize) {
202         short[] hashTable = HASH_TABLE.get();
203         if (hashTable == null || hashTable.length < hashTableSize) {
204             hashTable = new short[hashTableSize];
205             HASH_TABLE.set(hashTable);
206             return hashTable;
207         }
208 
209         Arrays.fill(hashTable, 0, hashTableSize, (short) 0);
210         return hashTable;
211     }
212 
213     /**
214      * Iterates over the supplied input buffer between the supplied minIndex and
215      * maxIndex to find how long our matched copy overlaps with an already-written
216      * literal value.
217      *
218      * @param in The input buffer to scan over
219      * @param minIndex The index in the input buffer to start scanning from
220      * @param inIndex The index of the start of our copy
221      * @param maxIndex The length of our input buffer
222      * @return The number of bytes for which our candidate copy is a repeat of
223      */
224     private static int findMatchingLength(ByteBuf in, int minIndex, int inIndex, int maxIndex) {
225         int matched = 0;
226 
227         while (inIndex <= maxIndex - 4 &&
228                 in.getInt(inIndex) == in.getInt(minIndex + matched)) {
229             inIndex += 4;
230             matched += 4;
231         }
232 
233         while (inIndex < maxIndex && in.getByte(minIndex + matched) == in.getByte(inIndex)) {
234             ++inIndex;
235             ++matched;
236         }
237 
238         return matched;
239     }
240 
241     /**
242      * Calculates the minimum number of bits required to encode a value.  This can
243      * then in turn be used to calculate the number of septets or octets (as
244      * appropriate) to use to encode a length parameter.
245      *
246      * @param value The value to calculate the minimum number of bits required to encode
247      * @return The minimum number of bits required to encode the supplied value
248      */
249     private static int bitsToEncode(int value) {
250         int highestOneBit = Integer.highestOneBit(value);
251         int bitLength = 0;
252         while ((highestOneBit >>= 1) != 0) {
253             bitLength++;
254         }
255 
256         return bitLength;
257     }
258 
259     /**
260      * Writes a literal to the supplied output buffer by directly copying from
261      * the input buffer.  The literal is taken from the current readerIndex
262      * up to the supplied length.
263      *
264      * @param in The input buffer to copy from
265      * @param out The output buffer to copy to
266      * @param length The length of the literal to copy
267      */
268     static void encodeLiteral(ByteBuf in, ByteBuf out, int length) {
269         if (length < 61) {
270             out.writeByte(length - 1 << 2);
271         } else {
272             int bitLength = bitsToEncode(length - 1);
273             int bytesToEncode = 1 + bitLength / 8;
274             out.writeByte(59 + bytesToEncode << 2);
275             for (int i = 0; i < bytesToEncode; i++) {
276                 out.writeByte(length - 1 >> i * 8 & 0x0ff);
277             }
278         }
279 
280         out.writeBytes(in, length);
281     }
282 
283     private static void encodeCopyWithOffset(ByteBuf out, int offset, int length) {
284         if (length < 12 && offset < 2048) {
285             out.writeByte(COPY_1_BYTE_OFFSET | length - 4 << 2 | offset >> 8 << 5);
286             out.writeByte(offset & 0x0ff);
287         } else {
288             out.writeByte(COPY_2_BYTE_OFFSET | length - 1 << 2);
289             out.writeByte(offset & 0x0ff);
290             out.writeByte(offset >> 8 & 0x0ff);
291         }
292     }
293 
294     /**
295      * Encodes a series of copies, each at most 64 bytes in length.
296      *
297      * @param out The output buffer to write the copy pointer to
298      * @param offset The offset at which the original instance lies
299      * @param length The length of the original instance
300      */
301     private static void encodeCopy(ByteBuf out, int offset, int length) {
302         while (length >= 68) {
303             encodeCopyWithOffset(out, offset, 64);
304             length -= 64;
305         }
306 
307         if (length > 64) {
308             encodeCopyWithOffset(out, offset, 60);
309             length -= 60;
310         }
311 
312         encodeCopyWithOffset(out, offset, length);
313     }
314 
315     public void decode(ByteBuf in, ByteBuf out) {
316         while (in.isReadable()) {
317             switch (state) {
318             case READING_PREAMBLE:
319                 int uncompressedLength = readPreamble(in);
320                 if (uncompressedLength == PREAMBLE_NOT_FULL) {
321                     // We've not yet read all of the preamble, so wait until we can
322                     return;
323                 }
324                 if (uncompressedLength == 0) {
325                     // Should never happen, but it does mean we have nothing further to do
326                     return;
327                 }
328                 out.ensureWritable(uncompressedLength);
329                 state = State.READING_TAG;
330                 // fall through
331             case READING_TAG:
332                 if (!in.isReadable()) {
333                     return;
334                 }
335                 tag = in.readByte();
336                 switch (tag & 0x03) {
337                 case LITERAL:
338                     state = State.READING_LITERAL;
339                     break;
340                 case COPY_1_BYTE_OFFSET:
341                 case COPY_2_BYTE_OFFSET:
342                 case COPY_4_BYTE_OFFSET:
343                     state = State.READING_COPY;
344                     break;
345                 }
346                 break;
347             case READING_LITERAL:
348                 int literalWritten = decodeLiteral(tag, in, out);
349                 if (literalWritten != NOT_ENOUGH_INPUT) {
350                     state = State.READING_TAG;
351                     written += literalWritten;
352                 } else {
353                     // Need to wait for more data
354                     return;
355                 }
356                 break;
357             case READING_COPY:
358                 int decodeWritten;
359                 switch (tag & 0x03) {
360                 case COPY_1_BYTE_OFFSET:
361                     decodeWritten = decodeCopyWith1ByteOffset(tag, in, out, written);
362                     if (decodeWritten != NOT_ENOUGH_INPUT) {
363                         state = State.READING_TAG;
364                         written += decodeWritten;
365                     } else {
366                         // Need to wait for more data
367                         return;
368                     }
369                     break;
370                 case COPY_2_BYTE_OFFSET:
371                     decodeWritten = decodeCopyWith2ByteOffset(tag, in, out, written);
372                     if (decodeWritten != NOT_ENOUGH_INPUT) {
373                         state = State.READING_TAG;
374                         written += decodeWritten;
375                     } else {
376                         // Need to wait for more data
377                         return;
378                     }
379                     break;
380                 case COPY_4_BYTE_OFFSET:
381                     decodeWritten = decodeCopyWith4ByteOffset(tag, in, out, written);
382                     if (decodeWritten != NOT_ENOUGH_INPUT) {
383                         state = State.READING_TAG;
384                         written += decodeWritten;
385                     } else {
386                         // Need to wait for more data
387                         return;
388                     }
389                     break;
390                 }
391             }
392         }
393     }
394 
395     /**
396      * Reads the length varint (a series of bytes, where the lower 7 bits
397      * are data and the upper bit is a flag to indicate more bytes to be
398      * read).
399      *
400      * @param in The input buffer to read the preamble from
401      * @return The calculated length based on the input buffer, or 0 if
402      *   no preamble is able to be calculated
403      */
404     private static int readPreamble(ByteBuf in) {
405         int length = 0;
406         int byteIndex = 0;
407         while (in.isReadable()) {
408             int current = in.readUnsignedByte();
409             length |= (current & 0x7f) << byteIndex++ * 7;
410             if ((current & 0x80) == 0) {
411                 return length;
412             }
413 
414             if (byteIndex >= 4) {
415                 throw new DecompressionException("Preamble is greater than 4 bytes");
416             }
417         }
418 
419         return 0;
420     }
421 
422     /**
423      * Get the length varint (a series of bytes, where the lower 7 bits
424      * are data and the upper bit is a flag to indicate more bytes to be
425      * read).
426      *
427      * @param in The input buffer to get the preamble from
428      * @return The calculated length based on the input buffer, or 0 if
429      *   no preamble is able to be calculated
430      */
431     int getPreamble(ByteBuf in) {
432         if (state == State.READING_PREAMBLE) {
433             int readerIndex = in.readerIndex();
434             try {
435                 return readPreamble(in);
436             } finally {
437                 in.readerIndex(readerIndex);
438             }
439         }
440         return 0;
441     }
442 
443     /**
444      * Reads a literal from the input buffer directly to the output buffer.
445      * A "literal" is an uncompressed segment of data stored directly in the
446      * byte stream.
447      *
448      * @param tag The tag that identified this segment as a literal is also
449      *            used to encode part of the length of the data
450      * @param in The input buffer to read the literal from
451      * @param out The output buffer to write the literal to
452      * @return The number of bytes appended to the output buffer, or -1 to indicate "try again later"
453      */
454     static int decodeLiteral(byte tag, ByteBuf in, ByteBuf out) {
455         in.markReaderIndex();
456         int length;
457         switch(tag >> 2 & 0x3F) {
458         case 60:
459             if (!in.isReadable()) {
460                 return NOT_ENOUGH_INPUT;
461             }
462             length = in.readUnsignedByte();
463             break;
464         case 61:
465             if (in.readableBytes() < 2) {
466                 return NOT_ENOUGH_INPUT;
467             }
468             length = in.readUnsignedShortLE();
469             break;
470         case 62:
471             if (in.readableBytes() < 3) {
472                 return NOT_ENOUGH_INPUT;
473             }
474             length = in.readUnsignedMediumLE();
475             break;
476         case 63:
477             if (in.readableBytes() < 4) {
478                 return NOT_ENOUGH_INPUT;
479             }
480             length = in.readIntLE();
481             break;
482         default:
483             length = tag >> 2 & 0x3F;
484         }
485         length += 1;
486 
487         if (in.readableBytes() < length) {
488             in.resetReaderIndex();
489             return NOT_ENOUGH_INPUT;
490         }
491 
492         out.writeBytes(in, length);
493         return length;
494     }
495 
496     /**
497      * Reads a compressed reference offset and length from the supplied input
498      * buffer, seeks back to the appropriate place in the input buffer and
499      * writes the found data to the supplied output stream.
500      *
501      * @param tag The tag used to identify this as a copy is also used to encode
502      *     the length and part of the offset
503      * @param in The input buffer to read from
504      * @param out The output buffer to write to
505      * @return The number of bytes appended to the output buffer, or -1 to indicate
506      *     "try again later"
507      * @throws DecompressionException If the read offset is invalid
508      */
509     private static int decodeCopyWith1ByteOffset(byte tag, ByteBuf in, ByteBuf out, int writtenSoFar) {
510         if (!in.isReadable()) {
511             return NOT_ENOUGH_INPUT;
512         }
513 
514         int initialIndex = out.writerIndex();
515         int length = 4 + ((tag & 0x01c) >> 2);
516         int offset = (tag & 0x0e0) << 8 >> 5 | in.readUnsignedByte();
517 
518         validateOffset(offset, writtenSoFar);
519 
520         out.markReaderIndex();
521         if (offset < length) {
522             int copies = length / offset;
523             for (; copies > 0; copies--) {
524                 out.readerIndex(initialIndex - offset);
525                 out.readBytes(out, offset);
526             }
527             if (length % offset != 0) {
528                 out.readerIndex(initialIndex - offset);
529                 out.readBytes(out, length % offset);
530             }
531         } else {
532             out.readerIndex(initialIndex - offset);
533             out.readBytes(out, length);
534         }
535         out.resetReaderIndex();
536 
537         return length;
538     }
539 
540     /**
541      * Reads a compressed reference offset and length from the supplied input
542      * buffer, seeks back to the appropriate place in the input buffer and
543      * writes the found data to the supplied output stream.
544      *
545      * @param tag The tag used to identify this as a copy is also used to encode
546      *     the length and part of the offset
547      * @param in The input buffer to read from
548      * @param out The output buffer to write to
549      * @throws DecompressionException If the read offset is invalid
550      * @return The number of bytes appended to the output buffer, or -1 to indicate
551      *     "try again later"
552      */
553     private static int decodeCopyWith2ByteOffset(byte tag, ByteBuf in, ByteBuf out, int writtenSoFar) {
554         if (in.readableBytes() < 2) {
555             return NOT_ENOUGH_INPUT;
556         }
557 
558         int initialIndex = out.writerIndex();
559         int length = 1 + (tag >> 2 & 0x03f);
560         int offset = in.readUnsignedShortLE();
561 
562         validateOffset(offset, writtenSoFar);
563 
564         out.markReaderIndex();
565         if (offset < length) {
566             int copies = length / offset;
567             for (; copies > 0; copies--) {
568                 out.readerIndex(initialIndex - offset);
569                 out.readBytes(out, offset);
570             }
571             if (length % offset != 0) {
572                 out.readerIndex(initialIndex - offset);
573                 out.readBytes(out, length % offset);
574             }
575         } else {
576             out.readerIndex(initialIndex - offset);
577             out.readBytes(out, length);
578         }
579         out.resetReaderIndex();
580 
581         return length;
582     }
583 
584     /**
585      * Reads a compressed reference offset and length from the supplied input
586      * buffer, seeks back to the appropriate place in the input buffer and
587      * writes the found data to the supplied output stream.
588      *
589      * @param tag The tag used to identify this as a copy is also used to encode
590      *     the length and part of the offset
591      * @param in The input buffer to read from
592      * @param out The output buffer to write to
593      * @return The number of bytes appended to the output buffer, or -1 to indicate
594      *     "try again later"
595      * @throws DecompressionException If the read offset is invalid
596      */
597     private static int decodeCopyWith4ByteOffset(byte tag, ByteBuf in, ByteBuf out, int writtenSoFar) {
598         if (in.readableBytes() < 4) {
599             return NOT_ENOUGH_INPUT;
600         }
601 
602         int initialIndex = out.writerIndex();
603         int length = 1 + (tag >> 2 & 0x03F);
604         int offset = in.readIntLE();
605 
606         validateOffset(offset, writtenSoFar);
607 
608         out.markReaderIndex();
609         if (offset < length) {
610             int copies = length / offset;
611             for (; copies > 0; copies--) {
612                 out.readerIndex(initialIndex - offset);
613                 out.readBytes(out, offset);
614             }
615             if (length % offset != 0) {
616                 out.readerIndex(initialIndex - offset);
617                 out.readBytes(out, length % offset);
618             }
619         } else {
620             out.readerIndex(initialIndex - offset);
621             out.readBytes(out, length);
622         }
623         out.resetReaderIndex();
624 
625         return length;
626     }
627 
628     /**
629      * Validates that the offset extracted from a compressed reference is within
630      * the permissible bounds of an offset (0 < offset < Integer.MAX_VALUE), and does not
631      * exceed the length of the chunk currently read so far.
632      *
633      * @param offset The offset extracted from the compressed reference
634      * @param chunkSizeSoFar The number of bytes read so far from this chunk
635      * @throws DecompressionException if the offset is invalid
636      */
637     private static void validateOffset(int offset, int chunkSizeSoFar) {
638         if (offset == 0) {
639             throw new DecompressionException("Offset is less than minimum permissible value");
640         }
641 
642         if (offset < 0) {
643             // Due to arithmetic overflow
644             throw new DecompressionException("Offset is greater than maximum value supported by this implementation");
645         }
646 
647         if (offset > chunkSizeSoFar) {
648             throw new DecompressionException("Offset exceeds size of chunk");
649         }
650     }
651 
652     /**
653      * Computes the CRC32C checksum of the supplied data and performs the "mask" operation
654      * on the computed checksum
655      *
656      * @param data The input data to calculate the CRC32C checksum of
657      */
658     static int calculateChecksum(ByteBuf data) {
659         return calculateChecksum(data, data.readerIndex(), data.readableBytes());
660     }
661 
662     /**
663      * Computes the CRC32C checksum of the supplied data and performs the "mask" operation
664      * on the computed checksum
665      *
666      * @param data The input data to calculate the CRC32C checksum of
667      */
668     static int calculateChecksum(ByteBuf data, int offset, int length) {
669         Crc32c crc32 = new Crc32c();
670         try {
671             crc32.update(data, offset, length);
672             return maskChecksum(crc32.getValue());
673         } finally {
674             crc32.reset();
675         }
676     }
677 
678     /**
679      * Computes the CRC32C checksum of the supplied data, performs the "mask" operation
680      * on the computed checksum, and then compares the resulting masked checksum to the
681      * supplied checksum.
682      *
683      * @param expectedChecksum The checksum decoded from the stream to compare against
684      * @param data The input data to calculate the CRC32C checksum of
685      * @throws DecompressionException If the calculated and supplied checksums do not match
686      */
687     static void validateChecksum(int expectedChecksum, ByteBuf data) {
688         validateChecksum(expectedChecksum, data, data.readerIndex(), data.readableBytes());
689     }
690 
691     /**
692      * Computes the CRC32C checksum of the supplied data, performs the "mask" operation
693      * on the computed checksum, and then compares the resulting masked checksum to the
694      * supplied checksum.
695      *
696      * @param expectedChecksum The checksum decoded from the stream to compare against
697      * @param data The input data to calculate the CRC32C checksum of
698      * @throws DecompressionException If the calculated and supplied checksums do not match
699      */
700     static void validateChecksum(int expectedChecksum, ByteBuf data, int offset, int length) {
701         final int actualChecksum = calculateChecksum(data, offset, length);
702         if (actualChecksum != expectedChecksum) {
703             throw new DecompressionException(
704                     "mismatching checksum: " + Integer.toHexString(actualChecksum) +
705                             " (expected: " + Integer.toHexString(expectedChecksum) + ')');
706         }
707     }
708 
709     /**
710      * From the spec:
711      *
712      * "Checksums are not stored directly, but masked, as checksumming data and
713      * then its own checksum can be problematic. The masking is the same as used
714      * in Apache Hadoop: Rotate the checksum by 15 bits, then add the constant
715      * 0xa282ead8 (using wraparound as normal for unsigned integers)."
716      *
717      * @param checksum The actual checksum of the data
718      * @return The masked checksum
719      */
720     static int maskChecksum(long checksum) {
721         return (int) ((checksum >> 15 | checksum << 17) + 0xa282ead8);
722     }
723 }