View Javadoc
1   /*
2    * Copyright 2019 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.netty5.handler.codec.compression;
17  
18  import io.netty5.buffer.api.Buffer;
19  import net.jpountz.xxhash.StreamingXXHash32;
20  import net.jpountz.xxhash.XXHash32;
21  import net.jpountz.xxhash.XXHashFactory;
22  
23  import java.nio.ByteBuffer;
24  import java.util.zip.Checksum;
25  
26  /**
27   * A special-purpose {@link BufferChecksum} implementation for use with
28   * {@link Lz4Compressor} and {@link Lz4Decompressor}.
29   *
30   * {@link StreamingXXHash32#asChecksum()} has a particularly nasty implementation
31   * of {@link Checksum#update(int)} that allocates a single-element byte array for
32   * every invocation.
33   *
34   * In addition to that, it doesn't implement an overload that accepts a {@link ByteBuffer}
35   * as an argument.
36   *
37   * Combined, this means that we can't use {@code ReflectiveByteBufChecksum} at all,
38   * and can't use {@code SlowByteBufChecksum} because of its atrocious performance
39   * with direct byte buffers (allocating an array and making a JNI call for every byte
40   * checksummed might be considered sub-optimal by some).
41   *
42   * Block version of xxHash32 ({@link XXHash32}), however, does provide
43   * {@link XXHash32#hash(ByteBuffer, int)} method that is efficient and does exactly
44   * what we need, with a caveat that we can only invoke it once before having to reset.
45   * This, however, is fine for our purposes, given the way we use it in
46   * {@link Lz4Compressor} and {@link Lz4Decompressor}:
47   * {@code reset()}, followed by one {@code update()}, followed by {@code getValue()}.
48   */
49  public final class Lz4XXHash32 extends BufferChecksum {
50  
51      private static final XXHash32 XXHASH32 = XXHashFactory.fastestInstance().hash32();
52  
53      private final int seed;
54      private boolean used;
55      private int value;
56  
57      @SuppressWarnings("WeakerAccess")
58      public Lz4XXHash32(int seed) {
59          super(new Checksum() {
60              @Override
61              public void update(int b) {
62                  throw new Error();
63              }
64  
65              @Override
66              public void update(byte[] b, int off, int len) {
67                  throw new Error();
68              }
69  
70              @Override
71              public long getValue() {
72                  throw new Error();
73              }
74  
75              @Override
76              public void reset() {
77                  throw new Error();
78              }
79          });
80          this.seed = seed;
81      }
82  
83      @Override
84      public void update(int b) {
85          update(new byte[] { (byte) b }, 0, 1);
86      }
87  
88      private void checkUsed() {
89          if (used) {
90              throw new IllegalStateException();
91          }
92      }
93  
94      @Override
95      public void update(byte[] b, int off, int len) {
96          checkUsed();
97          value = XXHASH32.hash(b, off, len, seed);
98          used = true;
99      }
100 
101     @Override
102     public void update(ByteBuffer buffer) {
103         checkUsed();
104         value = XXHASH32.hash(buffer, seed);
105         used = true;
106     }
107 
108     @Override
109     public void update(Buffer b, int off, int len) {
110         checkUsed();
111         if (b.countReadableComponents() > 1) {
112             byte[] bytes = new byte[len];
113             b.copyInto(off, bytes, 0, bytes.length);
114             value = XXHASH32.hash(bytes, 0, bytes.length, seed);
115         } else {
116             int oldOffset = b.readerOffset();
117             b.readerOffset(off);
118             try (var readableIteration = b.forEachReadable()) {
119                 for (var readableComponent = readableIteration.first();
120                      readableComponent != null; readableComponent = readableComponent.next()) {
121                     if (readableComponent.hasReadableArray()) {
122                         value = XXHASH32.hash(readableComponent.readableArray(),
123                                 readableComponent.readableArrayOffset(), len, seed);
124                     } else {
125                         ByteBuffer nioBuffer = readableComponent.readableBuffer();
126                         value = XXHASH32.hash(nioBuffer, nioBuffer.position(), len, seed);
127                     }
128                 }
129             } finally {
130                 b.readerOffset(oldOffset);
131             }
132         }
133         used = true;
134     }
135 
136     @Override
137     public long getValue() {
138         if (!used) {
139             throw new IllegalStateException();
140         }
141         /*
142          * If you look carefully, you'll notice that the most significant nibble
143          * is being discarded; we believe this to be a bug, but this is what
144          * StreamingXXHash32#asChecksum() implementation of getValue() does,
145          * so we have to retain this behaviour for compatibility reasons.
146          */
147         return value & 0xFFFFFFFL;
148     }
149 
150     @Override
151     public void reset() {
152         used = false;
153     }
154 }