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 }