Skip to content

Commit 3d567e1

Browse files
committed
MINOR: Close output stream when ParquetFileWriter construction fails on encryption validation
The shared ParquetFileWriter constructor opens the output stream early (via OutputFile.create/createOrOverwrite), then validates that every encrypted column named in the encryption properties exists in the file schema. When a configured encrypted column is absent, it throws ParquetCryptoRuntimeException. Because this validation runs after the stream is opened and is not guarded, the half-constructed writer is never returned to the caller, so its close() (the only place that closes the stream) can never run and the open stream leaks. A service that repeatedly builds writers with a misconfigured encryption schema accumulates leaked handles until exhaustion. Wrap the encryption setup in a try/catch that closes the already-opened stream before rethrowing, mirroring the existing guard in the ParquetFileReader constructor. Add a regression test that constructs a writer with an encrypted column missing from the schema and asserts the stream is closed on failure. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent 8e4f571 commit 3d567e1

2 files changed

Lines changed: 128 additions & 25 deletions

File tree

parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetFileWriter.java

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -519,36 +519,44 @@ private ParquetFileWriter(
519519
return;
520520
}
521521

522-
if (null == encryptionProperties) {
523-
encryptionProperties = encryptor.getEncryptionProperties();
524-
}
522+
try {
523+
if (null == encryptionProperties) {
524+
encryptionProperties = encryptor.getEncryptionProperties();
525+
}
525526

526-
// Verify that every encrypted column is in file schema
527-
Map<ColumnPath, ColumnEncryptionProperties> columnEncryptionProperties =
528-
encryptionProperties.getEncryptedColumns();
529-
if (null != columnEncryptionProperties) { // if null, every column in file schema will be encrypted with footer
530-
// key
531-
for (Map.Entry<ColumnPath, ColumnEncryptionProperties> entry : columnEncryptionProperties.entrySet()) {
532-
String[] path = entry.getKey().toArray();
533-
if (!schema.containsPath(path)) {
534-
StringBuilder columnList = new StringBuilder();
535-
columnList.append("[");
536-
for (String[] columnPath : schema.getPaths()) {
537-
columnList
538-
.append(ColumnPath.get(columnPath).toDotString())
539-
.append("], [");
527+
// Verify that every encrypted column is in file schema
528+
Map<ColumnPath, ColumnEncryptionProperties> columnEncryptionProperties =
529+
encryptionProperties.getEncryptedColumns();
530+
if (null != columnEncryptionProperties) { // if null, every column in file schema will be encrypted with
531+
// footer
532+
// key
533+
for (Map.Entry<ColumnPath, ColumnEncryptionProperties> entry : columnEncryptionProperties.entrySet()) {
534+
String[] path = entry.getKey().toArray();
535+
if (!schema.containsPath(path)) {
536+
StringBuilder columnList = new StringBuilder();
537+
columnList.append("[");
538+
for (String[] columnPath : schema.getPaths()) {
539+
columnList
540+
.append(ColumnPath.get(columnPath).toDotString())
541+
.append("], [");
542+
}
543+
throw new ParquetCryptoRuntimeException("Encrypted column ["
544+
+ entry.getKey().toDotString() + "] not in file schema column list: "
545+
+ columnList.substring(0, columnList.length() - 3));
540546
}
541-
throw new ParquetCryptoRuntimeException(
542-
"Encrypted column [" + entry.getKey().toDotString() + "] not in file schema column list: "
543-
+ columnList.substring(0, columnList.length() - 3));
544547
}
545548
}
546-
}
547549

548-
if (null == encryptor) {
549-
this.fileEncryptor = new InternalFileEncryptor(encryptionProperties);
550-
} else {
551-
this.fileEncryptor = encryptor;
550+
if (null == encryptor) {
551+
this.fileEncryptor = new InternalFileEncryptor(encryptionProperties);
552+
} else {
553+
this.fileEncryptor = encryptor;
554+
}
555+
} catch (Exception e) {
556+
// If encryption setup throws in the constructor, the output stream opened above should be
557+
// closed. Otherwise, there's no way to close it outside since the object is never returned.
558+
out.close();
559+
throw e;
552560
}
553561
}
554562

parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestParquetFileWriter.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import java.util.List;
5252
import java.util.Map;
5353
import java.util.concurrent.Callable;
54+
import java.util.concurrent.atomic.AtomicBoolean;
5455
import org.apache.hadoop.conf.Configuration;
5556
import org.apache.hadoop.fs.FSDataInputStream;
5657
import org.apache.hadoop.fs.FileStatus;
@@ -79,6 +80,9 @@
7980
import org.apache.parquet.column.statistics.LongStatistics;
8081
import org.apache.parquet.column.values.bloomfilter.BlockSplitBloomFilter;
8182
import org.apache.parquet.column.values.bloomfilter.BloomFilter;
83+
import org.apache.parquet.crypto.ColumnEncryptionProperties;
84+
import org.apache.parquet.crypto.FileEncryptionProperties;
85+
import org.apache.parquet.crypto.ParquetCryptoRuntimeException;
8286
import org.apache.parquet.example.data.Group;
8387
import org.apache.parquet.example.data.simple.SimpleGroup;
8488
import org.apache.parquet.format.Statistics;
@@ -87,6 +91,7 @@
8791
import org.apache.parquet.hadoop.example.GroupWriteSupport;
8892
import org.apache.parquet.hadoop.metadata.BlockMetaData;
8993
import org.apache.parquet.hadoop.metadata.ColumnChunkMetaData;
94+
import org.apache.parquet.hadoop.metadata.ColumnPath;
9095
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
9196
import org.apache.parquet.hadoop.metadata.ConcatenatingKeyValueMetadataMergeStrategy;
9297
import org.apache.parquet.hadoop.metadata.FileMetaData;
@@ -101,7 +106,9 @@
101106
import org.apache.parquet.internal.column.columnindex.BoundaryOrder;
102107
import org.apache.parquet.internal.column.columnindex.ColumnIndex;
103108
import org.apache.parquet.internal.column.columnindex.OffsetIndex;
109+
import org.apache.parquet.io.OutputFile;
104110
import org.apache.parquet.io.ParquetEncodingException;
111+
import org.apache.parquet.io.PositionOutputStream;
105112
import org.apache.parquet.io.api.Binary;
106113
import org.apache.parquet.schema.MessageType;
107114
import org.apache.parquet.schema.MessageTypeParser;
@@ -1498,6 +1505,94 @@ public void testMergeMetadataWithNoConflictingKeyValues() {
14981505
assertEquals("d", mergedValues.get("c"));
14991506
}
15001507

1508+
@Test
1509+
public void testConstructorClosesStreamWhenEncryptedColumnMissing() throws Exception {
1510+
ColumnEncryptionProperties missingColumn = ColumnEncryptionProperties.builder("not_in_schema")
1511+
.withKey("0123456789012345".getBytes(StandardCharsets.UTF_8))
1512+
.build();
1513+
Map<ColumnPath, ColumnEncryptionProperties> encryptedColumns = new HashMap<>();
1514+
encryptedColumns.put(missingColumn.getPath(), missingColumn);
1515+
FileEncryptionProperties encryptionProperties = FileEncryptionProperties.builder(
1516+
"0123456789012345".getBytes(StandardCharsets.UTF_8))
1517+
.withEncryptedColumns(encryptedColumns)
1518+
.build();
1519+
1520+
RecordingOutputFile file = new RecordingOutputFile();
1521+
try {
1522+
new ParquetFileWriter(
1523+
file,
1524+
SCHEMA,
1525+
CREATE,
1526+
DEFAULT_BLOCK_SIZE,
1527+
MAX_PADDING_SIZE_DEFAULT,
1528+
ParquetProperties.DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH,
1529+
ParquetProperties.DEFAULT_STATISTICS_TRUNCATE_LENGTH,
1530+
ParquetProperties.DEFAULT_PAGE_WRITE_CHECKSUM_ENABLED,
1531+
encryptionProperties);
1532+
fail("Expected ParquetCryptoRuntimeException for an encrypted column absent from the schema");
1533+
} catch (ParquetCryptoRuntimeException expected) {
1534+
// expected: the encrypted column is not in the file schema
1535+
}
1536+
1537+
assertTrue(
1538+
"The output stream opened by the constructor must be closed when validation fails",
1539+
file.isStreamClosed());
1540+
}
1541+
1542+
/**
1543+
* An {@link OutputFile} whose {@link PositionOutputStream} records whether it was closed, used to
1544+
* assert that a failed {@link ParquetFileWriter} construction does not leak the open stream.
1545+
*/
1546+
private static class RecordingOutputFile implements OutputFile {
1547+
1548+
private final AtomicBoolean streamClosed = new AtomicBoolean(false);
1549+
1550+
boolean isStreamClosed() {
1551+
return streamClosed.get();
1552+
}
1553+
1554+
private PositionOutputStream newRecordingStream() {
1555+
return new PositionOutputStream() {
1556+
private long pos = 0;
1557+
1558+
@Override
1559+
public long getPos() {
1560+
return pos;
1561+
}
1562+
1563+
@Override
1564+
public void write(int b) {
1565+
pos++;
1566+
}
1567+
1568+
@Override
1569+
public void close() {
1570+
streamClosed.set(true);
1571+
}
1572+
};
1573+
}
1574+
1575+
@Override
1576+
public PositionOutputStream create(long blockSizeHint) {
1577+
return newRecordingStream();
1578+
}
1579+
1580+
@Override
1581+
public PositionOutputStream createOrOverwrite(long blockSizeHint) {
1582+
return newRecordingStream();
1583+
}
1584+
1585+
@Override
1586+
public boolean supportsBlockSize() {
1587+
return false;
1588+
}
1589+
1590+
@Override
1591+
public long defaultBlockSize() {
1592+
return 0;
1593+
}
1594+
}
1595+
15011596
private org.apache.parquet.column.statistics.Statistics<?> statsC1(Binary... values) {
15021597
org.apache.parquet.column.statistics.Statistics<?> stats =
15031598
org.apache.parquet.column.statistics.Statistics.createStats(C1.getPrimitiveType());

0 commit comments

Comments
 (0)