001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.io.compress; 019 020import static org.junit.jupiter.api.Assertions.assertEquals; 021import static org.junit.jupiter.api.Assertions.assertFalse; 022import static org.junit.jupiter.api.Assertions.assertTrue; 023import static org.junit.jupiter.api.Assertions.fail; 024import static org.junit.jupiter.api.Assumptions.assumeTrue; 025 026import java.io.ByteArrayOutputStream; 027import java.io.IOException; 028import java.io.UncheckedIOException; 029import java.nio.ByteBuffer; 030import java.util.Arrays; 031import java.util.zip.GZIPOutputStream; 032import org.apache.hadoop.conf.Configuration; 033import org.apache.hadoop.hbase.nio.ByteBuff; 034import org.apache.hadoop.hbase.nio.MultiByteBuff; 035import org.apache.hadoop.hbase.nio.SingleByteBuff; 036import org.apache.hadoop.hbase.testclassification.SmallTests; 037import org.apache.hadoop.hbase.util.Bytes; 038import org.apache.hadoop.util.NativeCodeLoader; 039import org.junit.jupiter.api.Tag; 040import org.junit.jupiter.api.Test; 041 042@Tag(SmallTests.TAG) 043public class TestGzipByteBuffDecompressor { 044 045 // A single gzip member, reused as decompressor input across the tests. 046 private static final byte[] COMPRESSED_PAYLOAD = gzip("HBase is fun to use and very fast"); 047 048 /** 049 * GzipByteBuffDecompressor is backed by Hadoop's native zlib binding, so actually decompressing 050 * anything requires that native library to be loaded on this JVM. 051 */ 052 private static void assumeNativeZlibLoaded() { 053 assumeTrue(NativeCodeLoader.isNativeCodeLoaded(), 054 "Hadoop's native code is not loaded on this JVM, skipping"); 055 } 056 057 @Test 058 public void itReportsCorrectCapabilitiesWithoutNativeZlib() { 059 ByteBuff emptySingleDirectBuff = new SingleByteBuff(ByteBuffer.allocateDirect(0)); 060 ByteBuff emptySingleHeapBuff = new SingleByteBuff(ByteBuffer.allocate(0)); 061 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(false)) { 062 assertFalse(decompressor.canDecompress(emptySingleDirectBuff, emptySingleDirectBuff), 063 "Without native zlib, direct-to-direct decompression is not available"); 064 assertFalse(decompressor.canDecompress(emptySingleHeapBuff, emptySingleHeapBuff), 065 "Heap decompression is not supported; only direct-to-direct"); 066 } 067 } 068 069 @Test 070 public void itReportsCorrectCapabilitiesWithNativeZlib() { 071 assumeNativeZlibLoaded(); 072 ByteBuff emptySingleHeapBuff = new SingleByteBuff(ByteBuffer.allocate(0)); 073 ByteBuff emptyMultiHeapBuff = new MultiByteBuff(ByteBuffer.allocate(0), ByteBuffer.allocate(0)); 074 ByteBuff emptySingleDirectBuff = new SingleByteBuff(ByteBuffer.allocateDirect(0)); 075 ByteBuff emptyMultiDirectBuff = 076 new MultiByteBuff(ByteBuffer.allocateDirect(0), ByteBuffer.allocateDirect(0)); 077 078 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 079 assertTrue(decompressor.canDecompress(emptySingleDirectBuff, emptySingleDirectBuff)); 080 // Only direct-to-direct is supported; heap and mixed buffers return false. 081 assertFalse(decompressor.canDecompress(emptySingleHeapBuff, emptySingleHeapBuff)); 082 assertFalse(decompressor.canDecompress(emptySingleHeapBuff, emptySingleDirectBuff)); 083 assertFalse(decompressor.canDecompress(emptySingleDirectBuff, emptySingleHeapBuff)); 084 assertFalse(decompressor.canDecompress(emptyMultiHeapBuff, emptyMultiHeapBuff)); 085 assertFalse(decompressor.canDecompress(emptyMultiDirectBuff, emptyMultiDirectBuff)); 086 assertFalse(decompressor.canDecompress(emptySingleDirectBuff, emptyMultiDirectBuff)); 087 } 088 } 089 090 private static ByteBuff directBuffWith(byte[] data) { 091 ByteBuffer buffer = ByteBuffer.allocateDirect(data.length); 092 buffer.put(data); 093 buffer.rewind(); 094 return new SingleByteBuff(buffer); 095 } 096 097 private static ByteBuff heapBuffWith(byte[] data) { 098 ByteBuffer buffer = ByteBuffer.allocate(data.length); 099 buffer.put(data); 100 buffer.rewind(); 101 return new SingleByteBuff(buffer); 102 } 103 104 private static byte[] gzip(String text) { 105 ByteArrayOutputStream compressed = new ByteArrayOutputStream(); 106 try (GZIPOutputStream out = new GZIPOutputStream(compressed)) { 107 out.write(Bytes.toBytes(text)); 108 } catch (IOException e) { 109 throw new UncheckedIOException(e); 110 } 111 return compressed.toByteArray(); 112 } 113 114 private static byte[] concat(byte[] first, byte[] second) { 115 byte[] combined = new byte[first.length + second.length]; 116 System.arraycopy(first, 0, combined, 0, first.length); 117 System.arraycopy(second, 0, combined, first.length, second.length); 118 return combined; 119 } 120 121 @Test 122 public void itDecompressesDirectToDirectSuccessfully() throws IOException { 123 assumeNativeZlibLoaded(); 124 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 125 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 126 ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD); 127 int decompressedSize = decompressor.decompress(output, input, COMPRESSED_PAYLOAD.length); 128 assertEquals("HBase is fun to use and very fast", 129 Bytes.toString(output.toBytes(0, decompressedSize))); 130 } 131 } 132 133 @Test 134 public void itDecompressDirectFailsOnTooShortInput() throws IOException { 135 assumeNativeZlibLoaded(); 136 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 137 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 138 ByteBuff input = new SingleByteBuff(ByteBuffer.allocateDirect(10)); 139 decompressor.decompress(output, input, 10); 140 fail("Expected an IOException because the input is too short to be a gzip member"); 141 } catch (IOException e) { 142 assertTrue(e.getMessage().contains("too short to be a gzip member")); 143 } 144 } 145 146 @Test 147 public void itDecompressDirectFailsOnBadMagicBytes() throws IOException { 148 assumeNativeZlibLoaded(); 149 byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD, COMPRESSED_PAYLOAD.length); 150 corrupted[0] ^= (byte) 0xff; 151 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 152 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 153 ByteBuff input = directBuffWith(corrupted); 154 decompressor.decompress(output, input, corrupted.length); 155 fail("Expected an IOException because the magic bytes are wrong"); 156 } catch (IOException e) { 157 assertTrue(e.getMessage().contains("Invalid gzip stream")); 158 } 159 } 160 161 @Test 162 public void itDecompressDirectFailsWhenOutputBufferTooSmall() throws IOException { 163 assumeNativeZlibLoaded(); 164 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 165 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(10)); 166 ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD); 167 decompressor.decompress(output, input, COMPRESSED_PAYLOAD.length); 168 fail("Expected an IOException because the output buffer is too small"); 169 } catch (IOException e) { 170 assertTrue(e.getMessage().contains("Output buffer is too small")); 171 } 172 } 173 174 @Test 175 public void itDecompressDirectFailsOnCorruptedCrc32() throws IOException { 176 assumeNativeZlibLoaded(); 177 byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD, COMPRESSED_PAYLOAD.length); 178 // First 4 bytes of the 8-byte trailer are the CRC32, leave ISIZE (the last 4 bytes) alone. 179 corrupted[corrupted.length - 8] ^= (byte) 0xff; 180 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 181 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 182 ByteBuff input = directBuffWith(corrupted); 183 decompressor.decompress(output, input, corrupted.length); 184 fail("Expected an IOException because the trailer's CRC32 no longer matches"); 185 } catch (IOException e) { 186 assertTrue(e.getMessage().contains("Invalid gzip stream")); 187 } 188 } 189 190 @Test 191 public void itDecompressDirectFailsOnCorruptedIsize() throws IOException { 192 assumeNativeZlibLoaded(); 193 byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD, COMPRESSED_PAYLOAD.length); 194 // Last 4 bytes of the 8-byte trailer are the ISIZE. 195 corrupted[corrupted.length - 4] ^= (byte) 0xff; 196 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 197 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 198 ByteBuff input = directBuffWith(corrupted); 199 decompressor.decompress(output, input, corrupted.length); 200 fail("Expected an IOException because the trailer's ISIZE no longer matches"); 201 } catch (IOException e) { 202 assertTrue(e.getMessage().contains("Invalid gzip stream")); 203 } 204 } 205 206 @Test 207 public void itDecompressesDirectSuccessfullyOnRepeatedCalls() throws IOException { 208 assumeNativeZlibLoaded(); 209 // Mirrors how CodecPool actually uses these: one instance is reused across many blocks, so the 210 // native decompressor must produce a correct result on every call, not just the first. 211 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 212 for (int i = 0; i < 3; i++) { 213 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 214 ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD); 215 int decompressedSize = decompressor.decompress(output, input, COMPRESSED_PAYLOAD.length); 216 assertEquals("HBase is fun to use and very fast", 217 Bytes.toString(output.toBytes(0, decompressedSize))); 218 } 219 } 220 } 221 222 @Test 223 public void itDecompressDirectIsStillUsableAfterAPreviousCallThrows() throws IOException { 224 assumeNativeZlibLoaded(); 225 byte[] corrupted = Arrays.copyOf(COMPRESSED_PAYLOAD, COMPRESSED_PAYLOAD.length); 226 // First 4 bytes of the 8-byte trailer are the CRC32. 227 corrupted[corrupted.length - 8] ^= (byte) 0xff; 228 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 229 ByteBuff badOutput = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 230 ByteBuff badInput = directBuffWith(corrupted); 231 try { 232 decompressor.decompress(badOutput, badInput, corrupted.length); 233 fail("Expected an IOException because the trailer's CRC32 no longer matches"); 234 } catch (IOException e) { 235 assertTrue(e.getMessage().contains("Invalid gzip stream")); 236 } 237 238 // A prior failure must not leave the shared native decompressor state corrupted for the 239 // next, valid call. 240 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 241 ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD); 242 int decompressedSize = decompressor.decompress(output, input, COMPRESSED_PAYLOAD.length); 243 assertEquals("HBase is fun to use and very fast", 244 Bytes.toString(output.toBytes(0, decompressedSize))); 245 } 246 } 247 248 @Test 249 public void itDecompressesDirectToDirectWithNonZeroBufferPosition() throws IOException { 250 assumeNativeZlibLoaded(); 251 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 252 ByteBuffer rawOutput = ByteBuffer.allocateDirect(128); 253 rawOutput.position(32); 254 255 ByteBuffer rawInput = ByteBuffer.allocateDirect(16 + COMPRESSED_PAYLOAD.length); 256 for (int i = 0; i < 16; i++) { 257 rawInput.put((byte) 0); 258 } 259 rawInput.put(COMPRESSED_PAYLOAD); 260 rawInput.position(16); 261 262 ByteBuff output = new SingleByteBuff(rawOutput); 263 ByteBuff input = new SingleByteBuff(rawInput); 264 int decompressedSize = decompressor.decompress(output, input, COMPRESSED_PAYLOAD.length); 265 266 byte[] result = new byte[decompressedSize]; 267 rawOutput.position(32); 268 rawOutput.get(result); 269 assertEquals("HBase is fun to use and very fast", Bytes.toString(result)); 270 } 271 } 272 273 @Test 274 public void itDecompressDirectFailsOnTruncatedGzipStream() throws IOException { 275 assumeNativeZlibLoaded(); 276 byte[] truncated = Arrays.copyOf(COMPRESSED_PAYLOAD, COMPRESSED_PAYLOAD.length - 4); 277 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 278 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 279 ByteBuff input = directBuffWith(truncated); 280 decompressor.decompress(output, input, truncated.length); 281 fail("Expected an IOException because the gzip stream is truncated"); 282 } catch (IOException e) { 283 // Expected: the decompressor must not report finished() on an incomplete stream 284 } 285 } 286 287 @Test 288 public void itDecompressesOnlyTheDelimitedMemberFromAMultiMemberPayload() throws IOException { 289 assumeNativeZlibLoaded(); 290 // Two distinct members concatenated: we must decode only the one delimited by inputLen. 291 byte[] firstMember = gzip("first member"); 292 byte[] secondMember = gzip("second member"); 293 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 294 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 295 ByteBuff input = directBuffWith(concat(firstMember, secondMember)); 296 int decompressedSize = decompressor.decompress(output, input, firstMember.length); 297 assertEquals("first member", Bytes.toString(output.toBytes(0, decompressedSize))); 298 } 299 } 300 301 /** 302 * This is the exact gate {@code HFileBlockDefaultDecodingContext#canDecompressViaByteBuff} relies 303 * on to decide between ByteBuff decompression and the stream path, driven end-to-end from the 304 * {@code GzipHFileDecompressionContext#ALLOW_BYTE_BUFF_DECOMPRESSION_KEY} config flag. 305 */ 306 @Test 307 public void itReinitControlsByteBuffDecompressionViaConfigFlag() { 308 assumeNativeZlibLoaded(); 309 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 310 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 311 ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD); 312 313 Configuration conf = new Configuration(false); 314 conf.setBoolean(GzipHFileDecompressionContext.ALLOW_BYTE_BUFF_DECOMPRESSION_KEY, false); 315 decompressor.reinit(GzipHFileDecompressionContext.fromConfiguration(conf)); 316 assertFalse(decompressor.canDecompress(output, input), 317 "Block reader must fall back to stream decompression when the config flag " 318 + "disables ByteBuff decompression"); 319 320 conf.setBoolean(GzipHFileDecompressionContext.ALLOW_BYTE_BUFF_DECOMPRESSION_KEY, true); 321 decompressor.reinit(GzipHFileDecompressionContext.fromConfiguration(conf)); 322 assertTrue(decompressor.canDecompress(output, input), 323 "Block reader must use ByteBuff decompression when the config flag is enabled"); 324 325 // The default, with no config value set, must also allow ByteBuff decompression. 326 decompressor 327 .reinit(GzipHFileDecompressionContext.fromConfiguration(new Configuration(false))); 328 assertTrue(decompressor.canDecompress(output, input)); 329 } 330 } 331 332 @Test 333 public void itReinitWithNullContextIsNoOp() { 334 assumeNativeZlibLoaded(); 335 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(true)) { 336 ByteBuff output = new SingleByteBuff(ByteBuffer.allocateDirect(64)); 337 ByteBuff input = directBuffWith(COMPRESSED_PAYLOAD); 338 339 Configuration conf = new Configuration(false); 340 conf.setBoolean(GzipHFileDecompressionContext.ALLOW_BYTE_BUFF_DECOMPRESSION_KEY, false); 341 decompressor.reinit(GzipHFileDecompressionContext.fromConfiguration(conf)); 342 assertFalse(decompressor.canDecompress(output, input)); 343 344 decompressor.reinit(null); 345 assertFalse(decompressor.canDecompress(output, input), 346 "reinit(null) must not reset allowByteBuffDecompression back to the default"); 347 } 348 } 349 350 @Test 351 public void itReinitFailsOnWrongContextType() { 352 Compression.HFileDecompressionContext wrongContext = 353 new Compression.HFileDecompressionContext() { 354 @Override 355 public void close() { 356 } 357 358 @Override 359 public long heapSize() { 360 return 0; 361 } 362 }; 363 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(false)) { 364 decompressor.reinit(wrongContext); 365 fail("Expected an IllegalArgumentException because the context was not a " 366 + "GzipHFileDecompressionContext"); 367 } catch (IllegalArgumentException e) { 368 assertTrue(e.getMessage().contains("GzipHFileDecompressionContext")); 369 } 370 } 371 372 @Test 373 public void itDecompressThrowsWhenPassedAMultiByteBuff() throws IOException { 374 try (GzipByteBuffDecompressor decompressor = new GzipByteBuffDecompressor(false)) { 375 ByteBuff multiOutput = new MultiByteBuff(ByteBuffer.allocate(64), ByteBuffer.allocate(64)); 376 ByteBuff input = heapBuffWith(COMPRESSED_PAYLOAD); 377 decompressor.decompress(multiOutput, input, COMPRESSED_PAYLOAD.length); 378 fail("Expected an IllegalStateException when output is a MultiByteBuff"); 379 } catch (IllegalStateException e) { 380 assertTrue(e.getMessage().contains("not a SingleByteBuff")); 381 } 382 } 383 384}