Skip to content

Commit cbf9a2f

Browse files
committed
MINOR: Close output stream when ParquetFileWriter construction fails on encryption validation
The shared ParquetFileWriter constructor opens the output stream before validating that every encrypted column exists in the file schema. If validation fails, the half-constructed writer is never returned and the stream cannot be closed by the caller. Close the output stream before rethrowing encryption setup failures, matching ParquetFileReader. Add a regression test that verifies the stream is closed when an encrypted column is missing from the schema. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent 83c2c80 commit cbf9a2f

2 files changed

Lines changed: 123 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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.util.HashSet;
4949
import java.util.List;
5050
import java.util.Map;
51+
import java.util.concurrent.atomic.AtomicBoolean;
5152
import org.apache.hadoop.conf.Configuration;
5253
import org.apache.hadoop.fs.FSDataInputStream;
5354
import org.apache.hadoop.fs.FileStatus;
@@ -76,6 +77,9 @@
7677
import org.apache.parquet.column.statistics.LongStatistics;
7778
import org.apache.parquet.column.values.bloomfilter.BlockSplitBloomFilter;
7879
import org.apache.parquet.column.values.bloomfilter.BloomFilter;
80+
import org.apache.parquet.crypto.ColumnEncryptionProperties;
81+
import org.apache.parquet.crypto.FileEncryptionProperties;
82+
import org.apache.parquet.crypto.ParquetCryptoRuntimeException;
7983
import org.apache.parquet.example.data.Group;
8084
import org.apache.parquet.example.data.simple.SimpleGroup;
8185
import org.apache.parquet.format.Statistics;
@@ -84,6 +88,7 @@
8488
import org.apache.parquet.hadoop.example.GroupWriteSupport;
8589
import org.apache.parquet.hadoop.metadata.BlockMetaData;
8690
import org.apache.parquet.hadoop.metadata.ColumnChunkMetaData;
91+
import org.apache.parquet.hadoop.metadata.ColumnPath;
8792
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
8893
import org.apache.parquet.hadoop.metadata.ConcatenatingKeyValueMetadataMergeStrategy;
8994
import org.apache.parquet.hadoop.metadata.FileMetaData;
@@ -98,7 +103,9 @@
98103
import org.apache.parquet.internal.column.columnindex.BoundaryOrder;
99104
import org.apache.parquet.internal.column.columnindex.ColumnIndex;
100105
import org.apache.parquet.internal.column.columnindex.OffsetIndex;
106+
import org.apache.parquet.io.OutputFile;
101107
import org.apache.parquet.io.ParquetEncodingException;
108+
import org.apache.parquet.io.PositionOutputStream;
102109
import org.apache.parquet.io.api.Binary;
103110
import org.apache.parquet.schema.MessageType;
104111
import org.apache.parquet.schema.MessageTypeParser;
@@ -1443,6 +1450,89 @@ public void testMergeMetadataWithNoConflictingKeyValues(boolean vectoredRead) {
14431450
assertThat(mergedValues.get("c")).isEqualTo("d");
14441451
}
14451452

1453+
@Test
1454+
public void testConstructorClosesStreamWhenEncryptedColumnMissing() throws Exception {
1455+
ColumnEncryptionProperties missingColumn = ColumnEncryptionProperties.builder("not_in_schema")
1456+
.withKey("0123456789012345".getBytes(StandardCharsets.UTF_8))
1457+
.build();
1458+
Map<ColumnPath, ColumnEncryptionProperties> encryptedColumns = new HashMap<>();
1459+
encryptedColumns.put(missingColumn.getPath(), missingColumn);
1460+
FileEncryptionProperties encryptionProperties = FileEncryptionProperties.builder(
1461+
"0123456789012345".getBytes(StandardCharsets.UTF_8))
1462+
.withEncryptedColumns(encryptedColumns)
1463+
.build();
1464+
1465+
RecordingOutputFile file = new RecordingOutputFile();
1466+
assertThatThrownBy(() -> new ParquetFileWriter(
1467+
file,
1468+
SCHEMA,
1469+
CREATE,
1470+
DEFAULT_BLOCK_SIZE,
1471+
MAX_PADDING_SIZE_DEFAULT,
1472+
ParquetProperties.DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH,
1473+
ParquetProperties.DEFAULT_STATISTICS_TRUNCATE_LENGTH,
1474+
ParquetProperties.DEFAULT_PAGE_WRITE_CHECKSUM_ENABLED,
1475+
encryptionProperties))
1476+
.isInstanceOf(ParquetCryptoRuntimeException.class)
1477+
.hasMessage("Encrypted column [not_in_schema] not in file schema column list: [a.b], [c.d]");
1478+
1479+
assertThat(file.isStreamClosed()).isTrue();
1480+
}
1481+
1482+
/**
1483+
* An {@link OutputFile} whose {@link PositionOutputStream} records whether it was closed, used to
1484+
* assert that a failed {@link ParquetFileWriter} construction does not leak the open stream.
1485+
*/
1486+
private static class RecordingOutputFile implements OutputFile {
1487+
1488+
private final AtomicBoolean streamClosed = new AtomicBoolean(false);
1489+
1490+
boolean isStreamClosed() {
1491+
return streamClosed.get();
1492+
}
1493+
1494+
private PositionOutputStream newRecordingStream() {
1495+
return new PositionOutputStream() {
1496+
private long pos = 0;
1497+
1498+
@Override
1499+
public long getPos() {
1500+
return pos;
1501+
}
1502+
1503+
@Override
1504+
public void write(int b) {
1505+
pos++;
1506+
}
1507+
1508+
@Override
1509+
public void close() {
1510+
streamClosed.set(true);
1511+
}
1512+
};
1513+
}
1514+
1515+
@Override
1516+
public PositionOutputStream create(long blockSizeHint) {
1517+
return newRecordingStream();
1518+
}
1519+
1520+
@Override
1521+
public PositionOutputStream createOrOverwrite(long blockSizeHint) {
1522+
return newRecordingStream();
1523+
}
1524+
1525+
@Override
1526+
public boolean supportsBlockSize() {
1527+
return false;
1528+
}
1529+
1530+
@Override
1531+
public long defaultBlockSize() {
1532+
return 0;
1533+
}
1534+
}
1535+
14461536
private org.apache.parquet.column.statistics.Statistics<?> statsC1(Binary... values) {
14471537
org.apache.parquet.column.statistics.Statistics<?> stats =
14481538
org.apache.parquet.column.statistics.Statistics.createStats(C1.getPrimitiveType());

0 commit comments

Comments
 (0)