This is an automated email from the ASF dual-hosted git repository.
steveloughran pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/hadoop.git
The following commit(s) were added to refs/heads/trunk by this push:
new 5f5229f0d5b HADOOP-19936. TFile parsing is too brittle (#8594)
5f5229f0d5b is described below
commit 5f5229f0d5be4639172b9b12b18629078b3e54f0
Author: Steve Loughran <[email protected]>
AuthorDate: Thu Jul 23 12:46:55 2026 +0100
HADOOP-19936. TFile parsing is too brittle (#8594)
Contains content generated by Claude.ai
Contributed by Steve Loughran
---
.../org/apache/hadoop/io/file/tfile/BCFile.java | 30 +++-
.../org/apache/hadoop/io/file/tfile/TFile.java | 88 +++++++---
.../org/apache/hadoop/io/file/tfile/Utils.java | 31 +++-
.../hadoop/io/file/tfile/TestTFileByteArrays.java | 3 +
.../hadoop/io/file/tfile/TestTFileComparator2.java | 81 ++++-----
.../hadoop/io/file/tfile/TestTFileComparators.java | 73 +++-----
.../hadoop/io/file/tfile/TestTFileLoading.java | 189 +++++++++++++++++++++
7 files changed, 370 insertions(+), 125 deletions(-)
diff --git
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/BCFile.java
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/BCFile.java
index 43d829937e7..f43226b2c55 100644
---
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/BCFile.java
+++
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/BCFile.java
@@ -56,6 +56,9 @@ final class BCFile {
static final Version API_VERSION = new Version((short) 1, (short) 0);
static final Logger LOG = LoggerFactory.getLogger(BCFile.class);
+ // Largest encoded length accepted for the short metadata strings: {@value}.
+ private static final int MAX_META_STRING_LENGTH = 64 * 1024;
+
/**
* Prevent the instantiation of BCFile objects.
*/
@@ -636,11 +639,8 @@ public Reader(FSDataInputStream fin, long fileLength,
Configuration conf)
metaIndex = new MetaIndex(fin);
// read data:BCFile.index, the data block index
- BlockReader blockR = getMetaBlock(DataIndex.BLOCK_NAME);
- try {
+ try (BlockReader blockR = getMetaBlock(DataIndex.BLOCK_NAME)) {
dataIndex = new DataIndex(blockR);
- } finally {
- blockR.close();
}
}
@@ -771,6 +771,10 @@ public MetaIndex() {
// for read, construct the map from the file
public MetaIndex(DataInput in) throws IOException {
int count = Utils.readVInt(in);
+ if (count < 0) {
+ throw new IOException("Corrupted meta index: negative entry count "
+ + count);
+ }
index = new TreeMap<String, MetaIndexEntry>();
for (int nx = 0; nx < count; nx++) {
@@ -807,7 +811,7 @@ static final class MetaIndexEntry {
private final BlockRegion region;
public MetaIndexEntry(DataInput in) throws IOException {
- String fullMetaName = Utils.readString(in);
+ String fullMetaName = Utils.readString(in, MAX_META_STRING_LENGTH);
if (fullMetaName.startsWith(defaultPrefix)) {
metaName =
fullMetaName.substring(defaultPrefix.length(), fullMetaName
@@ -817,7 +821,8 @@ public MetaIndexEntry(DataInput in) throws IOException {
}
compressionAlgorithm =
- Compression.getCompressionAlgorithmByName(Utils.readString(in));
+ Compression.getCompressionAlgorithmByName(
+ Utils.readString(in, MAX_META_STRING_LENGTH));
region = new BlockRegion(in);
}
@@ -863,10 +868,17 @@ static class DataIndex {
// for read, deserialized from a file
public DataIndex(DataInput in) throws IOException {
defaultCompressionAlgorithm =
- Compression.getCompressionAlgorithmByName(Utils.readString(in));
+ Compression.getCompressionAlgorithmByName(
+ Utils.readString(in, MAX_META_STRING_LENGTH));
int n = Utils.readVInt(in);
- listRegions = new ArrayList<BlockRegion>(n);
+ if (n < 0) {
+ throw new IOException("Corrupted data index: negative block count "
+ + n);
+ }
+ // Only a capacity hint: bound it so a corrupt count cannot force a huge
+ // pre-allocation. The loop is limited by the bytes actually available.
+ listRegions = new ArrayList<>(Math.min(n, 1024));
for (int i = 0; i < n; i++) {
BlockRegion region = new BlockRegion(in);
@@ -879,7 +891,7 @@ public DataIndex(String defaultCompressionAlgorithmName) {
this.defaultCompressionAlgorithm =
Compression
.getCompressionAlgorithmByName(defaultCompressionAlgorithmName);
- listRegions = new ArrayList<BlockRegion>();
+ listRegions = new ArrayList<>();
}
public Algorithm getDefaultCompressionAlgorithm() {
diff --git
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/TFile.java
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/TFile.java
index aeacc16a78f..a686b5c0532 100644
---
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/TFile.java
+++
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/TFile.java
@@ -166,6 +166,21 @@ static int getFSOutputBufferSize(Configuration conf) {
/** comparator prefix: java class */
public static final String COMPARATOR_JCLASS = "jclass:";
+ /**
+ * {@value}.
+ */
+ public static final String TFILE_COMPARATOR_JCLASS_ENABLED =
+ "tfile.comparator.jclass.enabled";
+ public static final boolean TFILE_COMPARATOR_JCLASS_ENABLED_DEFAULT = false;
+
+ // Largest encoded length accepted for the comparator string on read.
+ private static final int MAX_COMPARATOR_LENGTH = 64 * 1024;
+ // Upper bound on the serialized size of one index entry: a key no larger
+ // than MAX_KEY_SIZE plus its vint/vlong length prefixes.
+ private static final int MAX_INDEX_ENTRY_SIZE = MAX_KEY_SIZE + 16;
+ // Cap on the pre-allocated capacity of index lists sized from file data.
+ private static final int INDEX_CAPACITY_HINT_CAP = 1024;
+
/**
* Make a raw comparator from a string name.
*
@@ -282,7 +297,7 @@ public Writer(FSDataOutputStream fsdos, int minBlockSize,
String compressName, String comparator, Configuration conf)
throws IOException {
sizeMinBlock = minBlockSize;
- tfileMeta = new TFileMeta(comparator);
+ tfileMeta = new TFileMeta(comparator, conf);
tfileIndex = new TFileIndex(tfileMeta.getComparator());
writerBCF = new BCFile.Writer(fsdos, compressName, conf);
@@ -804,11 +819,8 @@ public Reader(FSDataInputStream fsdis, long fileLength,
Configuration conf)
readerBCF = new BCFile.Reader(fsdis, fileLength, conf);
// first, read TFile meta
- BlockReader brMeta = readerBCF.getMetaBlock(TFileMeta.BLOCK_NAME);
- try {
- tfileMeta = new TFileMeta(brMeta);
- } finally {
- brMeta.close();
+ try (BlockReader brMeta = readerBCF.getMetaBlock(TFileMeta.BLOCK_NAME)) {
+ tfileMeta = new TFileMeta(brMeta, conf);
}
comparator = tfileMeta.getComparator();
@@ -1603,6 +1615,9 @@ void checkKey() throws IOException {
valueChecked = false;
klen = Utils.readVInt(blkReader);
+ if (klen < 0 || klen > MAX_KEY_SIZE) {
+ throw new IOException("Key length out of range: " + klen);
+ }
blkReader.readFully(keyBuffer, 0, klen);
valueBufferInputStream.reset(blkReader);
if (valueBufferInputStream.isLastChunk()) {
@@ -2048,27 +2063,32 @@ static final class TFileMeta {
private final BytesComparator comparator;
// ctor for writes
- public TFileMeta(String comparator) {
+ TFileMeta(String comparator, Configuration conf) {
// set fileVersion to API version when we create it.
version = TFile.API_VERSION;
recordCount = 0;
strComparator = (comparator == null) ? "" : comparator;
- this.comparator = makeComparator(strComparator);
+ this.comparator = makeComparator(strComparator, conf);
}
// ctor for reads
- public TFileMeta(DataInput in) throws IOException {
+ TFileMeta(DataInput in, Configuration conf) throws IOException {
version = new Version(in);
if (!version.compatibleWith(TFile.API_VERSION)) {
throw new RuntimeException("Incompatible TFile fileVersion.");
}
recordCount = Utils.readVLong(in);
- strComparator = Utils.readString(in);
- comparator = makeComparator(strComparator);
+ strComparator = Utils.readString(in, MAX_COMPARATOR_LENGTH);
+ comparator = makeComparator(strComparator, conf);
}
- @SuppressWarnings("unchecked")
static BytesComparator makeComparator(String comparator) {
+ return makeComparator(comparator, new Configuration());
+ }
+
+ @SuppressWarnings("unchecked")
+ static BytesComparator makeComparator(String comparator,
+ Configuration conf) {
if (comparator.length() == 0) {
// unsorted keys
return null;
@@ -2077,13 +2097,24 @@ static BytesComparator makeComparator(String
comparator) {
// default comparator
return new BytesComparator(new MemcmpRawComparator());
} else if (comparator.startsWith(COMPARATOR_JCLASS)) {
+ if (!conf.getBoolean(TFILE_COMPARATOR_JCLASS_ENABLED,
+ TFILE_COMPARATOR_JCLASS_ENABLED_DEFAULT)) {
+ throw new IllegalArgumentException(
+ "Class-name comparators are not enabled (set "
+ + TFILE_COMPARATOR_JCLASS_ENABLED + "=true to allow): "
+ + comparator);
+ }
String compClassName =
comparator.substring(COMPARATOR_JCLASS.length()).trim();
try {
- Class compClass = Class.forName(compClassName);
- // use its default ctor to create an instance
- return new BytesComparator((RawComparator<Object>) compClass
- .newInstance());
+ // Resolve without running the class initializer, confirm it really
+ // is a RawComparator, and only then load and construct it.
+ Class<?> compClass =
+ Class.forName(compClassName, false, conf.getClassLoader());
+ RawComparator<Object> rawComparator =
+ (RawComparator<Object>) compClass.asSubclass(RawComparator.class)
+ .getDeclaredConstructor().newInstance();
+ return new BytesComparator(rawComparator);
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to instantiate comparator: " + comparator + "("
@@ -2144,21 +2175,35 @@ static class TFileIndex {
*/
public TFileIndex(int entryCount, DataInput in, BytesComparator comparator)
throws IOException {
- index = new ArrayList<TFileIndexEntry>(entryCount);
- recordNumIndex = new ArrayList<Long>(entryCount);
+ // entryCount is derived from the file; only use it as a capacity hint,
+ // bounded, so a corrupt value cannot force a huge pre-allocation. The
+ // loops below are limited by the actual bytes available in the stream.
+ int capacityHint = Math.max(0, Math.min(entryCount,
INDEX_CAPACITY_HINT_CAP));
+ index = new ArrayList<>(capacityHint);
+ recordNumIndex = new ArrayList<>(capacityHint);
int size = Utils.readVInt(in); // size for the first key entry.
if (size > 0) {
+ if (size > MAX_INDEX_ENTRY_SIZE) {
+ throw new IOException("First key entry size out of range: " + size);
+ }
byte[] buffer = new byte[size];
in.readFully(buffer);
DataInputStream firstKeyInputStream =
new DataInputStream(new ByteArrayInputStream(buffer, 0, size));
int firstKeyLength = Utils.readVInt(firstKeyInputStream);
+ if (firstKeyLength < 0 || firstKeyLength > MAX_KEY_SIZE) {
+ throw new IOException("First key length out of range: "
+ + firstKeyLength);
+ }
firstKey = new ByteArray(new byte[firstKeyLength]);
firstKeyInputStream.readFully(firstKey.buffer());
for (int i = 0; i < entryCount; i++) {
size = Utils.readVInt(in);
+ if (size < 0 || size > MAX_INDEX_ENTRY_SIZE) {
+ throw new IOException("Index entry size out of range: " + size);
+ }
if (buffer.length < size) {
buffer = new byte[size];
}
@@ -2226,8 +2271,8 @@ public int upperBound(RawComparable key) {
* For writing to file.
*/
public TFileIndex(BytesComparator comparator) {
- index = new ArrayList<TFileIndexEntry>();
- recordNumIndex = new ArrayList<Long>();
+ index = new ArrayList<>();
+ recordNumIndex = new ArrayList<>();
this.comparator = comparator;
}
@@ -2301,6 +2346,9 @@ static final class TFileIndexEntry implements
RawComparable {
public TFileIndexEntry(DataInput in) throws IOException {
int len = Utils.readVInt(in);
+ if (len < 0 || len > MAX_KEY_SIZE) {
+ throw new IOException("Index entry key length out of range: " + len);
+ }
key = new byte[len];
in.readFully(key, 0, len);
kvEntries = Utils.readVLong(in);
diff --git
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/Utils.java
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/Utils.java
index 714dc5a12ac..10d0a548a53 100644
---
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/Utils.java
+++
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/file/tfile/Utils.java
@@ -19,6 +19,7 @@
import java.io.DataInput;
import java.io.DataOutput;
+import java.io.EOFException;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
@@ -274,8 +275,36 @@ public static void writeString(DataOutput out, String s)
throws IOException {
* @throws IOException raised on errors performing I/O.
*/
public static String readString(DataInput in) throws IOException {
+ return readString(in, -1);
+ }
+
+ /**
+ * Read a String as a VInt n, followed by n Bytes in Text format, rejecting
+ * lengths that are -2 or less. or larger than the supplied bound before any
+ * buffer is allocated.
+ * A length of -1 means "no data" and mapped to a null string.
+ *
+ * @param in The input stream.
+ * @param maxLength The largest permitted encoded length in bytes, negative
for no limit.
+ * @return The string or null.
+ * @throws EOFException input data length exceeds {@code maxLength}.
+ * @throws IOException IO failure.
+ * @throws NegativeArraySizeException string length was minus two or less.
+ */
+ public static String readString(DataInput in, int maxLength)
+ throws IOException {
int length = readVInt(in);
- if (length == -1) return null;
+ if (length == -1) {
+ return null;
+ }
+ if (length < 0) {
+ throw new NegativeArraySizeException("Corrupted data: negative string
length "
+ + length);
+ }
+ if (maxLength >= 0 && length > maxLength) {
+ throw new EOFException("String length " + length
+ + " exceeds the limit of " + maxLength);
+ }
byte[] buffer = new byte[length];
in.readFully(buffer);
return Text.decode(buffer);
diff --git
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileByteArrays.java
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileByteArrays.java
index 2661ce10186..3134e3a5e74 100644
---
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileByteArrays.java
+++
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileByteArrays.java
@@ -89,6 +89,9 @@ public void init(String compression, String comparator,
public void init(String compression, String comparator) {
this.compression = compression;
this.comparator = comparator;
+ if (comparator.startsWith(TFile.COMPARATOR_JCLASS)) {
+ conf.setBoolean(TFile.TFILE_COMPARATOR_JCLASS_ENABLED, true);
+ }
}
@BeforeEach
diff --git
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparator2.java
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparator2.java
index 4177d7362a1..212d240cd12 100644
---
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparator2.java
+++
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparator2.java
@@ -33,8 +33,8 @@
import static org.junit.jupiter.api.Assertions.*;
public class TestTFileComparator2 {
- private static String ROOT = GenericTestUtils.getTestDir().getAbsolutePath();
- private static final String name = "test-tfile-comparator2";
+ private static final String ROOT =
GenericTestUtils.getTestDir().getAbsolutePath();
+ private static final String NAME = "test-tfile-comparator2";
private final static int BLOCK_SIZE = 512;
private static final String VALUE = "value";
private static final String jClassLongWritableComparator = "jclass:"
@@ -52,55 +52,42 @@ private static String buildValue(long i) {
@Test
public void testSortedLongWritable() throws IOException {
Configuration conf = new Configuration();
- Path path = new Path(ROOT, name);
+ conf.setBoolean(TFile.TFILE_COMPARATOR_JCLASS_ENABLED, true);
+ Path path = new Path(ROOT, NAME);
FileSystem fs = path.getFileSystem(conf);
- FSDataOutputStream out = fs.create(path);
- try {
- TFile.Writer writer = new Writer(out, BLOCK_SIZE, "gz",
- jClassLongWritableComparator, conf);
- try {
- LongWritable key = new LongWritable(0);
- for (long i=0; i<NENTRY; ++i) {
- key.set(cube(i-NENTRY/2));
- DataOutputStream dos = writer.prepareAppendKey(-1);
- try {
- key.write(dos);
- } finally {
- dos.close();
- }
- dos = writer.prepareAppendValue(-1);
- try {
- dos.write(buildValue(i).getBytes());
- } finally {
- dos.close();
- }
+ try (FSDataOutputStream out = fs.create(path);
+ Writer writer = new Writer(out, BLOCK_SIZE, "gz",
+ jClassLongWritableComparator, conf)) {
+ LongWritable key = new LongWritable(0);
+ for (long i = 0; i < NENTRY; ++i) {
+ key.set(cube(i - NENTRY / 2));
+ DataOutputStream dos = writer.prepareAppendKey(-1);
+ try {
+ key.write(dos);
+ } finally {
+ dos.close();
}
- } finally {
- writer.close();
- }
- } finally {
- out.close();
- }
-
- FSDataInputStream in = fs.open(path);
- try {
- TFile.Reader reader = new TFile.Reader(in, fs.getFileStatus(path)
- .getLen(), conf);
- try {
- TFile.Reader.Scanner scanner = reader.createScanner();
- long i=0;
- BytesWritable value = new BytesWritable();
- for (; !scanner.atEnd(); scanner.advance()) {
- scanner.entry().getValue(value);
- assertEquals(buildValue(i), new String(value.getBytes(), 0, value
- .getLength()));
- ++i;
+ dos = writer.prepareAppendValue(-1);
+ try {
+ dos.write(buildValue(i).getBytes());
+ } finally {
+ dos.close();
}
- } finally {
- reader.close();
}
- } finally {
- in.close();
+ }
+ final long len = fs.getFileStatus(path).getLen();
+
+ try (FSDataInputStream in = fs.open(path);
+ TFile.Reader reader = new TFile.Reader(in, len, conf)) {
+ TFile.Reader.Scanner scanner = reader.createScanner();
+ long i = 0;
+ BytesWritable value = new BytesWritable();
+ for (; !scanner.atEnd(); scanner.advance()) {
+ scanner.entry().getValue(value);
+ assertEquals(buildValue(i), new String(value.getBytes(), 0, value
+ .getLength()));
+ ++i;
+ }
}
}
}
diff --git
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparators.java
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparators.java
index f349b581461..54f36866e66 100644
---
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparators.java
+++
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileComparators.java
@@ -17,8 +17,6 @@
package org.apache.hadoop.io.file.tfile;
-import static org.junit.jupiter.api.Assertions.fail;
-
import java.io.IOException;
import org.junit.jupiter.api.AfterEach;
@@ -32,6 +30,8 @@
import org.apache.hadoop.io.file.tfile.TFile.Writer;
import org.apache.hadoop.test.GenericTestUtils;
+import static org.apache.hadoop.test.LambdaTestUtils.intercept;
+
/**
*
* Byte arrays test case class using GZ compression codec, base class of none
@@ -39,79 +39,56 @@
*
*/
public class TestTFileComparators {
- private static String ROOT = GenericTestUtils.getTestDir().getAbsolutePath();
- private final static int BLOCK_SIZE = 512;
+ private static final String ROOT =
GenericTestUtils.getTestDir().getAbsolutePath();
+ private static final int BLOCK_SIZE = 512;
private FileSystem fs;
private Configuration conf;
private Path path;
private FSDataOutputStream out;
private Writer writer;
- private String compression = Compression.Algorithm.GZ.getName();
- private String outputFile = "TFileTestComparators";
- /*
- * pre-sampled numbers of records in one block, based on the given the
- * generated key and value strings
- */
- // private int records1stBlock = 4314;
- // private int records2ndBlock = 4108;
- private int records1stBlock = 4480;
- private int records2ndBlock = 4263;
+ private static final String COMPRESSION = Compression.Algorithm.GZ.getName();
+ private static final String OUTPUT_FILE = "TFileTestComparators";
+
@BeforeEach
public void setUp() throws IOException {
conf = new Configuration();
- path = new Path(ROOT, outputFile);
+ path = new Path(ROOT, OUTPUT_FILE);
fs = path.getFileSystem(conf);
out = fs.create(path);
}
@AfterEach
public void tearDown() throws IOException {
+ closeOutput();
fs.delete(path, true);
}
// bad comparator format
@Test
- public void testFailureBadComparatorNames() throws IOException {
- try {
- writer = new Writer(out, BLOCK_SIZE, compression, "badcmp", conf);
- fail("Failed to catch unsupported comparator names");
- }
- catch (Exception e) {
- // noop, expecting exceptions
- e.printStackTrace();
- }
+ public void testFailureBadComparatorNames() throws Exception {
+ intercept(IllegalArgumentException.class, "Unsupported comparator", () ->
+ new Writer(out, BLOCK_SIZE, COMPRESSION, "badcmp", conf));
}
- // jclass that doesn't exist
+ // jclass that doesn't exist: fails to instantiate, not because the feature
+ // is disabled.
@Test
- public void testFailureBadJClassNames() throws IOException {
- try {
- writer =
- new Writer(out, BLOCK_SIZE, compression,
- "jclass: some.non.existence.clazz", conf);
- fail("Failed to catch unsupported comparator names");
- }
- catch (Exception e) {
- // noop, expecting exceptions
- e.printStackTrace();
- }
+ public void testFailureBadJClassNames() throws Exception {
+ conf.setBoolean(TFile.TFILE_COMPARATOR_JCLASS_ENABLED, true);
+ intercept(IllegalArgumentException.class, "Failed to instantiate
comparator",
+ () -> new Writer(out, BLOCK_SIZE, COMPRESSION,
+ "jclass: some.non.existence.clazz", conf));
}
- // class exists but not a RawComparator
+ // class exists but is not a RawComparator
@Test
- public void testFailureBadJClasses() throws IOException {
- try {
- writer =
- new Writer(out, BLOCK_SIZE, compression,
- "jclass:org.apache.hadoop.io.file.tfile.Chunk", conf);
- fail("Failed to catch unsupported comparator names");
- }
- catch (Exception e) {
- // noop, expecting exceptions
- e.printStackTrace();
- }
+ public void testFailureBadJClasses() throws Exception {
+ conf.setBoolean(TFile.TFILE_COMPARATOR_JCLASS_ENABLED, true);
+ intercept(IllegalArgumentException.class, "Failed to instantiate
comparator",
+ () -> new Writer(out, BLOCK_SIZE, COMPRESSION,
+ "jclass:org.apache.hadoop.io.file.tfile.Chunk", conf));
}
private void closeOutput() throws IOException {
diff --git
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileLoading.java
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileLoading.java
new file mode 100644
index 00000000000..a3e03e971d7
--- /dev/null
+++
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestTFileLoading.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
under
+ * the License.
+ */
+
+package org.apache.hadoop.io.file.tfile;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FSDataInputStream;
+import org.apache.hadoop.fs.FSDataOutputStream;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.io.LongWritable;
+import org.apache.hadoop.io.file.tfile.TFile.Reader;
+import org.apache.hadoop.io.file.tfile.TFile.Writer;
+import org.apache.hadoop.test.GenericTestUtils;
+
+import static org.apache.hadoop.test.LambdaTestUtils.intercept;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test TFile loading resilience.
+ */
+public class TestTFileLoading {
+
+ private static final String ROOT =
+ GenericTestUtils.getTestDir().getAbsolutePath();
+
+ private static final int BLOCK_SIZE = 512;
+
+ private static final String COMPRESSION =
Compression.Algorithm.NONE.getName();
+
+ private static final String JCLASS_COMPARATOR =
+ TFile.COMPARATOR_JCLASS + LongWritable.Comparator.class.getName();
+
+ private FileSystem fs;
+
+ private Path path;
+
+ @BeforeEach
+ public void setUp() throws IOException {
+ Configuration conf = new Configuration();
+ path = new Path(ROOT, "TestTFileLoading");
+ fs = FileSystem.getLocal(conf);
+ }
+
+ @AfterEach
+ public void tearDown() throws IOException {
+ fs.delete(path, true);
+ }
+
+ private Configuration enableJclass() {
+ Configuration conf = new Configuration();
+ conf.setBoolean(TFile.TFILE_COMPARATOR_JCLASS_ENABLED, true);
+ return conf;
+ }
+
+ @Test
+ public void testJClassComparatorRejectedByDefault() throws Exception {
+ try (FSDataOutputStream out = fs.create(path)) {
+ intercept(IllegalArgumentException.class, () ->
+ new Writer(out, BLOCK_SIZE, COMPRESSION, JCLASS_COMPARATOR, new
Configuration()));
+ }
+ }
+
+ @Test
+ public void testJClassComparatorMustBeRawComparator() throws Exception {
+ Configuration conf = enableJclass();
+ String notAComparator =
+ TFile.COMPARATOR_JCLASS + Chunk.class.getName();
+ try (FSDataOutputStream out = fs.create(path)) {
+ intercept(IllegalArgumentException.class, () ->
+ new Writer(out, BLOCK_SIZE, COMPRESSION, notAComparator, conf));
+ }
+ }
+
+ /**
+ * When jclass support is disabled, tfile readers fail to load files
+ * containing them.
+ */
+ @Test
+ public void testReaderRejectsJClassWhenDisabled() throws Exception {
+ // write a valid, sorted file naming a jclass comparator.
+ try (FSDataOutputStream out = fs.create(path);
+ Writer writer = new Writer(out, BLOCK_SIZE, COMPRESSION,
JCLASS_COMPARATOR,
+ enableJclass())) {
+ LongWritable key = new LongWritable(0);
+ for (long i = 0; i < 4; ++i) {
+ key.set(i);
+ try (DataOutputStream dos = writer.prepareAppendKey(-1)) {
+ key.write(dos);
+ }
+ try (DataOutputStream dos = writer.prepareAppendValue(-1)) {
+ dos.write(("value-" + i).getBytes());
+ }
+ }
+ }
+
+ long len = fs.getFileStatus(path).getLen();
+ // a reader that has not enabled the feature refuses the file.
+ Configuration disabled = new Configuration();
+ try (FSDataInputStream in = fs.open(path)) {
+ intercept(IllegalArgumentException.class, () ->
+ new Reader(in, len, disabled));
+ }
+ // with the feature enabled the same file opens.
+ try (FSDataInputStream in = fs.open(path)) {
+ Reader reader = new Reader(in, len, enableJclass());
+ assertThat(reader.getComparator())
+ .describedAs("comparator")
+ .isNotNull();
+ reader.close();
+ }
+ }
+
+ /**
+ * A bounded string read rejects an over-long length before allocating.
+ */
+ @Test
+ public void testReadStringRejectsOversizedLength() throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ // claim a huge length but supply no payload.
+ Utils.writeVInt(new DataOutputStream(bos), Integer.MAX_VALUE);
+ DataInputStream in =
+ new DataInputStream(new ByteArrayInputStream(bos.toByteArray()));
+ intercept(EOFException.class, () -> Utils.readString(in, 1024));
+ }
+
+ /**
+ * A string length less than minus 1 is rejected.
+ */
+ @Test
+ public void testReadStringRejectsNegativeLength() throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ Utils.writeVInt(new DataOutputStream(bos), -2);
+ DataInputStream in =
+ new DataInputStream(new ByteArrayInputStream(bos.toByteArray()));
+ intercept(NegativeArraySizeException.class, () -> Utils.readString(in));
+ }
+
+ /**
+ * A length of -1 is null; this is the classic behavior.
+ */
+ @Test
+ public void testReadStringMinus1MapsToNull() throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ Utils.writeVInt(new DataOutputStream(bos), -1);
+ DataInputStream in =
+ new DataInputStream(new ByteArrayInputStream(bos.toByteArray()));
+ assertThat(Utils.readString(in)).isNull();
+ }
+
+ /**
+ * Reject index entry records out of range.
+ */
+ @Test
+ public void testIndexEntryKeyLengthBounded() throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ // a key length well beyond the 64KB maximum.
+ Utils.writeVInt(new DataOutputStream(bos), 1 << 20);
+ DataInputStream in =
+ new DataInputStream(new ByteArrayInputStream(bos.toByteArray()));
+ intercept(IOException.class, () ->
+ new TFile.TFileIndexEntry(in));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]