From 2eb19a9bf5a5f2654dc258d2e65cca9d40a416d7 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Fri, 3 Oct 2025 21:47:30 +0530 Subject: [PATCH 1/6] 8367561: introduce tests --- .../file/FileURLConnStreamLeakTest.java | 234 ++++++++++++++++++ .../www/protocol/file/GetInputStreamTest.java | 150 +++++++++++ 2 files changed, 384 insertions(+) create mode 100644 test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java create mode 100644 test/jdk/sun/net/www/protocol/file/GetInputStreamTest.java diff --git a/test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java b/test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java new file mode 100644 index 0000000000000..ad0f32114d952 --- /dev/null +++ b/test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.InputStream; +import java.net.URLConnection; +import java.net.UnknownServiceException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8367561 + * @summary verify that the implementation of URLConnection APIs for "file:" + * protocol does not leak InputStream(s) + * @run junit/othervm ${test.main.class} + */ +class FileURLConnStreamLeakTest { + + private static final String FILE_URLCONNECTION_CLASSNAME = "sun.net.www.protocol.file.FileURLConnection"; + + private Path testFile; + + @BeforeEach + void beforeEach() throws Exception { + final Path file = Files.createTempFile(Path.of("."), "8367561-", ".txt"); + Files.writeString(file, String.valueOf(System.currentTimeMillis())); + this.testFile = file; + } + + @AfterEach + void afterEach() throws Exception { + Files.deleteIfExists(this.testFile); + } + + + @Test + void testGetContentEncoding() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getContentEncoding(); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetContentLength() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getContentLength(); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetContentLengthLong() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getContentLengthLong(); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetContentType() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getContentType(); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetDate() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getDate(); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetExpiration() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getExpiration(); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetHeaderField() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getHeaderField(0); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetHeaderFieldString() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final String val = conn.getHeaderField("foo"); + assertNull(val, "unexpected header field value: " + val); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetHeaderFieldDate() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getHeaderFieldDate("bar", 42); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetHeaderFieldInt() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final int val = conn.getHeaderFieldInt("hello", 42); + assertEquals(42, val, "unexpected header value"); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetHeaderFieldKey() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final String val = conn.getHeaderFieldKey(42); + assertNull(val, "unexpected header value: " + val); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetHeaderFieldLong() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final long val = conn.getHeaderFieldLong("foo", 42); + assertEquals(42, val, "unexpected header value"); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetHeaderFields() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final Map> headers = conn.getHeaderFields(); + assertNotNull(headers, "null headers"); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetLastModified() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + final var _ = conn.getLastModified(); + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetInputStream() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + try (final InputStream is = conn.getInputStream()) { + assertNotNull(is, "input stream is null"); + } + Files.delete(this.testFile); // must not fail + } + + @Test + void testGetOutputStream() throws Exception { + final URLConnection conn = this.testFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection for " + this.testFile + " is null"); + assertEquals(FILE_URLCONNECTION_CLASSNAME, conn.getClass().getName(), + "unexpected URLConnection type"); + // FileURLConnection only supports reading + assertThrows(UnknownServiceException.class, conn::getOutputStream); + Files.delete(this.testFile); // must not fail + } +} + diff --git a/test/jdk/sun/net/www/protocol/file/GetInputStreamTest.java b/test/jdk/sun/net/www/protocol/file/GetInputStreamTest.java new file mode 100644 index 0000000000000..25c1ee2e965b7 --- /dev/null +++ b/test/jdk/sun/net/www/protocol/file/GetInputStreamTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.Collator; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary verify the behaviour of URLConnection.getInputStream() + * for "file:" protocol + * @run junit ${test.main.class} + */ +class GetInputStreamTest { + + /** + * Calls URLConnection.getInputStream() on the URLConnection for a directory and verifies + * the contents returned by the InputStream. + */ + @Test + void testDirInputStream() throws Exception { + final Path dir = Files.createTempDirectory(Path.of("."), "fileurlconn-"); + final int numEntries = 3; + // write some files into that directory + for (int i = 1; i <= numEntries; i++) { + Files.writeString(dir.resolve(i + ".txt"), "" + i); + } + final String expectedDirListing = getDirListing(dir.toFile(), numEntries); + final URLConnection conn = dir.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection is null for " + dir); + // call getInputStream() and verify that the streamed directory + // listing is the expected one + try (final InputStream is = conn.getInputStream()) { + assertNotNull(is, "InputStream is null for " + conn); + final String actual = new BufferedReader(new InputStreamReader(is)) + .readAllAsString(); + assertEquals(expectedDirListing, actual, + "unexpected content from input stream for dir " + dir); + } + // now that we successfully obtained the InputStream, read its content + // and closed it, call getInputStream() again and verify that it can no longer + // be used to read any more content. + try (final InputStream is = conn.getInputStream()) { + assertNotNull(is, "input stream is null for " + conn); + final int readByte = is.read(); + assertEquals(-1, readByte, "expected to have read EOF from the stream"); + } + } + + /** + * Calls URLConnection.getInputStream() on the URLConnection for a regular file and verifies + * the contents returned by the InputStream. + */ + @Test + void testRegularFileInputStream() throws Exception { + final Path dir = Files.createTempDirectory(Path.of("."), "fileurlconn-"); + final Path regularFile = dir.resolve("foo.txt"); + final String expectedContent = "bar"; + Files.writeString(regularFile, expectedContent); + + final URLConnection conn = regularFile.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection is null for " + regularFile); + // get the input stream and verify the streamed content + try (final InputStream is = conn.getInputStream()) { + assertNotNull(is, "input stream is null for " + conn); + final String actual = new BufferedReader(new InputStreamReader(is)) + .readAllAsString(); + assertEquals(expectedContent, actual, + "unexpected content from input stream for file " + regularFile); + } + // now that we successfully obtained the InputStream, read its content + // and closed it, call getInputStream() again and verify that it can no longer + // be used to read any more content. + try (final InputStream is = conn.getInputStream()) { + assertNotNull(is, "input stream is null for " + conn); + // for regular files the FileURLConnection's InputStream throws a IOException + // when attempting to read after EOF + final IOException thrown = assertThrows(IOException.class, is::read); + final String exMessage = thrown.getMessage(); + assertEquals("Stream closed", exMessage, "unexpected exception message"); + } + } + + /** + * Verifies that URLConnection.getInputStream() for a non-existent file path + * throws FileNotFoundException. + */ + @Test + void testNonExistentFile() throws Exception { + final Path existentDir = Files.createTempDirectory(Path.of("."), "fileurlconn-"); + final Path nonExistent = existentDir.resolve("non-existent"); + final URLConnection conn = nonExistent.toUri().toURL().openConnection(); + assertNotNull(conn, "URLConnection is null for " + nonExistent); + final FileNotFoundException thrown = assertThrows(FileNotFoundException.class, + conn::getInputStream); + final String exMessage = thrown.getMessage(); + assertTrue(exMessage != null && exMessage.contains(nonExistent.getFileName().toString()), + "unexpected exception message: " + exMessage); + } + + private static String getDirListing(final File dir, final int numExpectedEntries) { + final List dirListing = Arrays.asList(dir.list()); + dirListing.sort(Collator.getInstance()); // same as what FileURLConnection does + + assertEquals(numExpectedEntries, dirListing.size(), + dir + " - expected " + numExpectedEntries + " entries but found: " + dirListing); + + final StringBuilder sb = new StringBuilder(); + for (String fileName : dirListing) { + sb.append(fileName); + sb.append("\n"); + } + return sb.toString(); + } +} From 74439cc7070d8c98c06d80f1e0386cb2a5f90e9a Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Fri, 3 Oct 2025 21:47:51 +0530 Subject: [PATCH 2/6] 8367561: fix inputstream leak --- .../classes/sun/net/www/URLConnection.java | 4 +- .../www/protocol/file/FileURLConnection.java | 148 ++++++++++++++---- 2 files changed, 120 insertions(+), 32 deletions(-) diff --git a/src/java.base/share/classes/sun/net/www/URLConnection.java b/src/java.base/share/classes/sun/net/www/URLConnection.java index 66005ab9b2afe..54a97dbaa1089 100644 --- a/src/java.base/share/classes/sun/net/www/URLConnection.java +++ b/src/java.base/share/classes/sun/net/www/URLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -111,7 +111,7 @@ public String getHeaderField(String name) { } - Map> headerFields; + private Map> headerFields; @Override public Map> getHeaderFields() { diff --git a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java index fc947f8977fa9..f5fde5e180f6d 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java @@ -25,15 +25,30 @@ package sun.net.www.protocol.file; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.net.FileNameMap; import java.net.MalformedURLException; import java.net.URL; -import java.net.FileNameMap; -import java.io.*; -import java.text.Collator; import java.security.Permission; -import sun.net.www.*; -import java.util.*; +import java.text.Collator; import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import sun.net.www.MessageHeader; +import sun.net.www.ParseUtil; +import sun.net.www.URLConnection; /** * Open a file input stream given a URL. @@ -61,31 +76,55 @@ public class FileURLConnection extends URLConnection { private long length = -1; private long lastModified = 0; + private Map> headerFields; protected FileURLConnection(URL u, File file) { super(u); this.file = file; } - /* + /** + * If already connected, then this method is a no-op. + * If not already connected, then this method does + * readability checks for the File. + *

+ * If the File is a directory then the readability check + * is done by verifying that File.list() does not return + * null. On the other hand, if the File is not a directory, + * then this method constructs a temporary FileInputStream + * for the File and lets the FileInputStream's constructor + * implementation do the necessary readability checks. + * That temporary FileInputStream is closed before returning + * from this method. + *

+ * In either case, if the readability checks fail, then + * an IOException is thrown from this method and the + * FileURLConnection stays unconnected. + *

+ * A normal return from this method implies that the + * FileURLConnection is connected and the readability + * checks have passed for the File. + *

* Note: the semantics of FileURLConnection object is that the * results of the various URLConnection calls, such as * getContentType, getInputStream or getContentLength reflect * whatever was true when connect was called. */ + @Override public void connect() throws IOException { if (!connected) { - isDirectory = file.isDirectory(); + // verify readability of the directory or the regular file if (isDirectory) { String[] fileList = file.list(); - if (fileList == null) + if (fileList == null) { throw new FileNotFoundException(file.getPath() + " exists, but is not accessible"); + } directoryListing = Arrays.asList(fileList); } else { - is = new BufferedInputStream(new FileInputStream(file.getPath())); + try (var _ = new FileInputStream(file.getPath())) { + } } - connected = true; } } @@ -135,21 +174,42 @@ private void initializeHeaders() { } } - public Map> getHeaderFields() { + @Override + public Map> getHeaderFields() { initializeHeaders(); - return super.getHeaderFields(); + if (headerFields == null) { + if (!isReadable()) { + return super.getHeaderFields(); + } + if (properties == null) { + headerFields = super.getHeaderFields(); + } else { + headerFields = properties.getHeaders(); + } + } + return headerFields; } + @Override public String getHeaderField(String name) { initializeHeaders(); - return super.getHeaderField(name); + if (!isReadable()) { + return null; + } + return properties == null ? null : properties.findValue(name); } + @Override public String getHeaderField(int n) { initializeHeaders(); - return super.getHeaderField(n); + if (!isReadable()) { + return null; + } + MessageHeader props = properties; + return props == null ? null : props.getValue(n); } + @Override public int getContentLength() { initializeHeaders(); if (length > Integer.MAX_VALUE) @@ -157,54 +217,82 @@ public int getContentLength() { return (int) length; } + @Override public long getContentLengthLong() { initializeHeaders(); return length; } + @Override public String getHeaderFieldKey(int n) { initializeHeaders(); - return super.getHeaderFieldKey(n); + if (!isReadable()) { + return null; + } + MessageHeader props = properties; + return props == null ? null : props.getKey(n); } + @Override public MessageHeader getProperties() { initializeHeaders(); return super.getProperties(); } + @Override public long getLastModified() { initializeHeaders(); return lastModified; } + @Override public synchronized InputStream getInputStream() throws IOException { connect(); + // connect() does the necessary readability checks and is expected to + // throw IOException if any of those checks fail. A normal completion of connect() + // must mean that connect succeeded. + assert connected : "not connected"; - if (is == null) { - if (isDirectory) { + // a FileURLConnection only ever creates and provides a single InputStream + if (is != null) { + return is; + } - if (directoryListing == null) { - throw new FileNotFoundException(file.getPath()); - } + if (isDirectory) { + // a successful connect() implies the directoryListing is non-null + // if the file is a directory + assert directoryListing != null : "missing directory listing"; - directoryListing.sort(Collator.getInstance()); + directoryListing.sort(Collator.getInstance()); - StringBuilder sb = new StringBuilder(); - for (String fileName : directoryListing) { - sb.append(fileName); - sb.append("\n"); - } - // Put it into a (default) locale-specific byte-stream. - is = new ByteArrayInputStream(sb.toString().getBytes()); - } else { - throw new FileNotFoundException(file.getPath()); + StringBuilder sb = new StringBuilder(); + for (String fileName : directoryListing) { + sb.append(fileName); + sb.append("\n"); } + // Put it into a (default) locale-specific byte-stream. + is = new ByteArrayInputStream(sb.toString().getBytes()); + } else { + is = new BufferedInputStream(new FileInputStream(file.getPath())); } return is; } + private synchronized boolean isReadable() { + try { + // connect() (if not already connected) does the readability checks + // and throws an IOException if those checks fail. A successful + // completion from connect() implies the File is readable. + connect(); + } catch (IOException e) { + return false; + } + return true; + } + + Permission permission; /* since getOutputStream isn't supported, only read permission is From 0703f7f5da67a531a160bb699473c885a346a412 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Sat, 4 Oct 2025 21:59:30 +0530 Subject: [PATCH 3/6] missed pushing the change --- .../classes/sun/net/www/protocol/file/FileURLConnection.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java index f5fde5e180f6d..2e8c1a5f01bee 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java @@ -40,6 +40,7 @@ import java.text.Collator; import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -179,10 +180,10 @@ public Map> getHeaderFields() { initializeHeaders(); if (headerFields == null) { if (!isReadable()) { - return super.getHeaderFields(); + return Collections.emptyMap(); } if (properties == null) { - headerFields = super.getHeaderFields(); + headerFields = Collections.emptyMap(); } else { headerFields = properties.getHeaders(); } From 92382cd6b27951cb95dc3c3d72ff851a6f1b3b9b Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Tue, 7 Oct 2025 12:50:49 +0530 Subject: [PATCH 4/6] Daniel's suggestion - don't repeat/copy code from super() --- .../classes/sun/net/www/URLConnection.java | 22 +++++++-- .../www/protocol/file/FileURLConnection.java | 47 ++++--------------- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/java.base/share/classes/sun/net/www/URLConnection.java b/src/java.base/share/classes/sun/net/www/URLConnection.java index 54a97dbaa1089..becbf88da730d 100644 --- a/src/java.base/share/classes/sun/net/www/URLConnection.java +++ b/src/java.base/share/classes/sun/net/www/URLConnection.java @@ -101,9 +101,21 @@ public Map> getRequestProperties() { return Collections.emptyMap(); } + /** + * This method is called whenever the headers related methods are called on the + * {@code URLConnection}. This method does any necessary checks and initializations + * to make sure that the headers can be served. If this {@code URLConnection} cannot + * serve the headers, then this method throws an {@code IOException}. + * + * @throws IOException if the headers cannot be served + */ + protected void ensureCanServeHeaders() throws IOException { + getInputStream(); + } + public String getHeaderField(String name) { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return null; } @@ -117,7 +129,7 @@ public String getHeaderField(String name) { public Map> getHeaderFields() { if (headerFields == null) { try { - getInputStream(); + ensureCanServeHeaders(); if (properties == null) { headerFields = super.getHeaderFields(); } else { @@ -137,7 +149,7 @@ public Map> getHeaderFields() { */ public String getHeaderFieldKey(int n) { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return null; } @@ -152,7 +164,7 @@ public String getHeaderFieldKey(int n) { */ public String getHeaderField(int n) { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return null; } @@ -221,7 +233,7 @@ public void setContentType(String type) { */ public int getContentLength() { try { - getInputStream(); + ensureCanServeHeaders(); } catch (Exception e) { return -1; } diff --git a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java index 2e8c1a5f01bee..3ac1c6a3cc492 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java @@ -40,7 +40,6 @@ import java.text.Collator; import java.text.SimpleDateFormat; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -77,7 +76,6 @@ public class FileURLConnection extends URLConnection { private long length = -1; private long lastModified = 0; - private Map> headerFields; protected FileURLConnection(URL u, File file) { super(u); @@ -178,36 +176,19 @@ private void initializeHeaders() { @Override public Map> getHeaderFields() { initializeHeaders(); - if (headerFields == null) { - if (!isReadable()) { - return Collections.emptyMap(); - } - if (properties == null) { - headerFields = Collections.emptyMap(); - } else { - headerFields = properties.getHeaders(); - } - } - return headerFields; + return super.getHeaderFields(); } @Override public String getHeaderField(String name) { initializeHeaders(); - if (!isReadable()) { - return null; - } - return properties == null ? null : properties.findValue(name); + return super.getHeaderField(name); } @Override public String getHeaderField(int n) { initializeHeaders(); - if (!isReadable()) { - return null; - } - MessageHeader props = properties; - return props == null ? null : props.getValue(n); + return super.getHeaderField(n); } @Override @@ -227,11 +208,7 @@ public long getContentLengthLong() { @Override public String getHeaderFieldKey(int n) { initializeHeaders(); - if (!isReadable()) { - return null; - } - MessageHeader props = properties; - return props == null ? null : props.getKey(n); + return super.getHeaderFieldKey(n); } @Override @@ -281,16 +258,12 @@ public synchronized InputStream getInputStream() return is; } - private synchronized boolean isReadable() { - try { - // connect() (if not already connected) does the readability checks - // and throws an IOException if those checks fail. A successful - // completion from connect() implies the File is readable. - connect(); - } catch (IOException e) { - return false; - } - return true; + @Override + protected synchronized void ensureCanServeHeaders() throws IOException { + // connect() (if not already connected) does the readability checks + // and throws an IOException if those checks fail. A successful + // completion from connect() implies the File is readable. + connect(); } From 799e41b05e154fd22525cf41074d6d27bb435603 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Wed, 8 Oct 2025 11:55:03 +0530 Subject: [PATCH 5/6] Volkan's review --- .../classes/sun/net/www/protocol/file/FileURLConnection.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java index 3ac1c6a3cc492..85a50b639cf56 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/file/FileURLConnection.java @@ -121,8 +121,9 @@ public void connect() throws IOException { } directoryListing = Arrays.asList(fileList); } else { - try (var _ = new FileInputStream(file.getPath())) { - } + // let FileInputStream constructor do the necessary readability checks + // and propagate any failures + new FileInputStream(file.getPath()).close(); } connected = true; } From 0f23e390cab5ddcb9172773aaf26b2e5d3054477 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Wed, 8 Oct 2025 15:49:41 +0530 Subject: [PATCH 6/6] Daniel's suggestion - add reachability fence --- .../file/FileURLConnStreamLeakTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java b/test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java index ad0f32114d952..2d6eb001b9ca9 100644 --- a/test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java +++ b/test/jdk/sun/net/www/protocol/file/FileURLConnStreamLeakTest.java @@ -22,6 +22,7 @@ */ import java.io.InputStream; +import java.lang.ref.Reference; import java.net.URLConnection; import java.net.UnknownServiceException; import java.nio.file.Files; @@ -71,6 +72,7 @@ void testGetContentEncoding() throws Exception { "unexpected URLConnection type"); final var _ = conn.getContentEncoding(); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -81,6 +83,7 @@ void testGetContentLength() throws Exception { "unexpected URLConnection type"); final var _ = conn.getContentLength(); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -91,6 +94,7 @@ void testGetContentLengthLong() throws Exception { "unexpected URLConnection type"); final var _ = conn.getContentLengthLong(); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -101,6 +105,7 @@ void testGetContentType() throws Exception { "unexpected URLConnection type"); final var _ = conn.getContentType(); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -111,6 +116,7 @@ void testGetDate() throws Exception { "unexpected URLConnection type"); final var _ = conn.getDate(); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -121,6 +127,7 @@ void testGetExpiration() throws Exception { "unexpected URLConnection type"); final var _ = conn.getExpiration(); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -131,6 +138,7 @@ void testGetHeaderField() throws Exception { "unexpected URLConnection type"); final var _ = conn.getHeaderField(0); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -142,6 +150,7 @@ void testGetHeaderFieldString() throws Exception { final String val = conn.getHeaderField("foo"); assertNull(val, "unexpected header field value: " + val); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -152,6 +161,7 @@ void testGetHeaderFieldDate() throws Exception { "unexpected URLConnection type"); final var _ = conn.getHeaderFieldDate("bar", 42); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -163,6 +173,7 @@ void testGetHeaderFieldInt() throws Exception { final int val = conn.getHeaderFieldInt("hello", 42); assertEquals(42, val, "unexpected header value"); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -174,6 +185,7 @@ void testGetHeaderFieldKey() throws Exception { final String val = conn.getHeaderFieldKey(42); assertNull(val, "unexpected header value: " + val); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -185,6 +197,7 @@ void testGetHeaderFieldLong() throws Exception { final long val = conn.getHeaderFieldLong("foo", 42); assertEquals(42, val, "unexpected header value"); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -196,6 +209,7 @@ void testGetHeaderFields() throws Exception { final Map> headers = conn.getHeaderFields(); assertNotNull(headers, "null headers"); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -206,6 +220,7 @@ void testGetLastModified() throws Exception { "unexpected URLConnection type"); final var _ = conn.getLastModified(); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -218,6 +233,7 @@ void testGetInputStream() throws Exception { assertNotNull(is, "input stream is null"); } Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } @Test @@ -229,6 +245,7 @@ void testGetOutputStream() throws Exception { // FileURLConnection only supports reading assertThrows(UnknownServiceException.class, conn::getOutputStream); Files.delete(this.testFile); // must not fail + Reference.reachabilityFence(conn); } }