This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-csv.git
The following commit(s) were added to refs/heads/master by this push:
new f1bf8cd2 Close the stream parse(Path) and parse(URL) open on
construction failure (#630).
f1bf8cd2 is described below
commit f1bf8cd22d97d99efa7b554cffa3e8144bd14de3
Author: Gary Gregory <[email protected]>
AuthorDate: Wed Aug 5 14:53:52 2026 -0400
Close the stream parse(Path) and parse(URL) open on construction failure
(#630).
---
src/changes/changes.xml | 1 +
.../java/org/apache/commons/csv/CSVParser.java | 19 ++++++--
.../java/org/apache/commons/csv/CSVParserTest.java | 55 ++++++++++++++++++++++
3 files changed, 72 insertions(+), 3 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 482c5c98..efaa89b3 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -72,6 +72,7 @@
<action type="fix" dev="ggregory" due-to="Gary Gregory, saleem
malik">Fill the lookahead buffer on short reads from a chunked source
(#625).</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory, Naveed
Khan">Handle null name in CSVRecord accessors under ignoreHeaderCase
(#628).</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory, Naveed
Khan">Quote null value that starts a record in minimal quote mode
(#629).</action>
+ <action type="fix" dev="ggregory" due-to="Gary Gregory, Naveed
Khan">Close the stream parse(Path) and parse(URL) open on construction failure
(#630).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary Gregory, Indy, Sylvia van
Os" issue="CSV-307">Add an "Android Compatibility" section to the web
site.</action>
<action type="add" dev="ggregory" due-to="Ruiqi Dong, Gary Gregory"
issue="CSV-325">Add CSVParser.Builder.setByteOffset(long) (#604).</action>
diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java
b/src/main/java/org/apache/commons/csv/CSVParser.java
index 891539e8..6d2a3b99 100644
--- a/src/main/java/org/apache/commons/csv/CSVParser.java
+++ b/src/main/java/org/apache/commons/csv/CSVParser.java
@@ -50,7 +50,9 @@ import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.commons.io.Charsets;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.io.build.AbstractStreamBuilder;
+import org.apache.commons.io.function.IOSupplier;
import org.apache.commons.io.function.Uncheck;
/**
@@ -371,6 +373,18 @@ public final class CSVParser implements
Iterable<CSVRecord>, Closeable {
return parse(new InputStreamReader(inputStream,
Charsets.toCharset(charset)), format);
}
+ private static CSVParser parse(final IOSupplier<InputStream> supplier,
final Charset charset, final CSVFormat format) throws IOException {
+ Objects.requireNonNull(supplier, "supplier");
+ final InputStream inputStream = supplier.get();
+ try {
+ return parse(inputStream, charset, format);
+ } catch (final IOException | RuntimeException e) {
+ // This method allocated the stream and the caller never gets a
parser to close, so close it here.
+ IOUtils.closeQuietlySuppress(inputStream, e);
+ throw e;
+ }
+ }
+
/**
* Creates and returns a parser for the given {@link Path}, which the
caller MUST close.
*
@@ -392,7 +406,7 @@ public final class CSVParser implements
Iterable<CSVRecord>, Closeable {
@SuppressWarnings("resource")
public static CSVParser parse(final Path path, final Charset charset,
final CSVFormat format) throws IOException {
Objects.requireNonNull(path, "path");
- return parse(Files.newInputStream(path), charset, format);
+ return parse(() -> Files.newInputStream(path), charset, format);
}
/**
@@ -461,10 +475,9 @@ public final class CSVParser implements
Iterable<CSVRecord>, Closeable {
* @throws CSVException Thrown on invalid CSV input data.
* @throws NullPointerException if {@code url} is {@code null}.
*/
- @SuppressWarnings("resource")
public static CSVParser parse(final URL url, final Charset charset, final
CSVFormat format) throws IOException {
Objects.requireNonNull(url, "url");
- return parse(url.openStream(), charset, format);
+ return parse(url::openStream, charset, format);
}
private String headerComment;
diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java
b/src/test/java/org/apache/commons/csv/CSVParserTest.java
index a0204142..af364ebd 100644
--- a/src/test/java/org/apache/commons/csv/CSVParserTest.java
+++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java
@@ -30,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -45,6 +46,8 @@ import java.io.StringReader;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -308,6 +311,57 @@ class CSVParserTest {
assertThrows(NoSuchElementException.class, records::next);
}
+ @Test
+ void testClosesInputStreamOnParsePathException() throws IOException {
+ final Path path = Files.createTempFile(getClass().getName(), ".csv");
+ try {
+ Files.write(path, "A,,C\n1,2,3\n".getBytes(UTF_8));
+ final CSVFormat format =
CSVFormat.DEFAULT.builder().setHeader().get();
+ assertThrows(IllegalArgumentException.class, () ->
CSVParser.parse(path, UTF_8, format));
+ } finally {
+ Files.delete(path);
+ }
+ }
+
+ @Test
+ void testClosesInputStreamOnParseUrlException() throws IOException {
+ final AtomicBoolean closed = new AtomicBoolean();
+ final URLStreamHandler handler = new URLStreamHandler() {
+
+ @Override
+ protected URLConnection openConnection(final URL u) {
+ return new URLConnection(u) {
+
+ @Override
+ public void connect() {
+ // noop
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new FilterInputStream(new
ByteArrayInputStream("A,,C\n1,2,3\n".getBytes(UTF_8))) {
+
+ @Override
+ public void close() throws IOException {
+ closed.set(true);
+ super.close();
+ }
+ };
+ }
+ };
+ }
+ };
+ final URL url = new URL("csv", null, -1, "test.csv", handler);
+ final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get();
+ assertThrows(IllegalArgumentException.class, () -> {
+ try (CSVParser parser = CSVParser.parse(url, UTF_8, format)) {
+ // we never get here
+ fail("The parser should not be constructed when the header is
invalid");
+ }
+ });
+ assertTrue(closed.get(), "The stream opened from the URL must be
closed when the parser cannot be constructed");
+ }
+
@Test
void testCSV141CSVFormat_DEFAULT() throws Exception {
testCSV141Failure(CSVFormat.DEFAULT, 3);
@@ -2065,6 +2119,7 @@ class CSVParserTest {
}
}
+
@Test
void testTryWithResourcesParseInputStreamWhenHeaderIsInvalid() throws
IOException {
final AtomicBoolean closed = new AtomicBoolean();