From dac0b6564d5135c6615e0501d44c0abcfccfae81 Mon Sep 17 00:00:00 2001 From: salmane Date: Sun, 15 Feb 2026 22:27:15 +0000 Subject: [PATCH 1/4] Add java tests to build path to put the test in the appropriate directory, id have to java files to grade build configs, because it currently only checks for kotlin tests. --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4f91e6d98..741461bb4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,6 +41,9 @@ sourceSets{ } } test{ + java { + srcDirs("test") + } kotlin{ srcDirs("test") } From 1b08eec808c45e17ef4640b5d26d6d3a8dad00a8 Mon Sep 17 00:00:00 2001 From: salmane Date: Sun, 15 Feb 2026 22:40:20 +0000 Subject: [PATCH 2/4] Unit test for rsrc leakage in unzip create a temp zip file > create a destination that is a file not a directory (guaranteed exception) -> unzip throws ioexception because it expects a directory not a file -> catch it -> check if the zip file is still open -> if true == leak. --- app/test/processing/app/UtilTest.java | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 app/test/processing/app/UtilTest.java diff --git a/app/test/processing/app/UtilTest.java b/app/test/processing/app/UtilTest.java new file mode 100644 index 000000000..c394d1cdf --- /dev/null +++ b/app/test/processing/app/UtilTest.java @@ -0,0 +1,81 @@ +package processing.app; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assumptions; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class UtilTest { + + @Test + public void unzipLeaksFileDescriptorsOnException() throws IOException { + // thi only runs on Linux where /proc/self/fd exists otherwise skip + Assumptions.assumeTrue(new File("/proc/self/fd").exists(), + "Skipping test: /proc/self/fd not available (not Linux)"); + // create a temporary zip file here with one entry + File zipFile = File.createTempFile("leak-test", ".zip"); + zipFile.deleteOnExit(); + File destDir = File.createTempFile("dest", ""); + destDir.delete(); // turn into a directory + destDir.mkdirs(); + destDir.deleteOnExit(); + // build a simple zip file + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + ZipEntry entry = new ZipEntry("test.txt"); + zos.putNextEntry(entry); + zos.write("hello".getBytes()); + zos.closeEntry(); + } + // make the destination directory read‑only – this will cause extraction to fail + destDir.setReadOnly(); + boolean exceptionThrown = false; + try { + Util.unzip(zipFile, destDir); + } catch (IOException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown, "Expected an exception because destDir is read‑only"); + + // check if the file is open by examining /proc/self/fd symlinks + boolean fileStillOpen = isFileOpen(zipFile); + assertFalse(fileStillOpen, "File " + zipFile + " is still open after exception – leak detected"); + + destDir.setWritable(true); + destDir.delete(); + zipFile.delete(); + } + + /** + * Checks whether the given file is currently open by the current process. + * Works on Linux by reading the symlinks in /proc/self/fd. + */ + private boolean isFileOpen(File file) throws IOException { + Path fdDir = Paths.get("/proc/self/fd"); + String targetPath = file.getCanonicalPath(); + + try (Stream fdPaths = Files.list(fdDir)) { + return fdPaths + .map(Path::toFile) + .map(File::toPath) + .map(path -> { + try { + return Files.readSymbolicLink(path); + } catch (IOException e) { + return null; // not a symlink or inaccessible + } + }) + .filter(resolved -> resolved != null) + .anyMatch(resolved -> resolved.toString().equals(targetPath)); + } + } +} \ No newline at end of file From 5a79e32d5e453c6181626099c63ab5d051066c65 Mon Sep 17 00:00:00 2001 From: salmane Date: Sun, 15 Feb 2026 22:43:41 +0000 Subject: [PATCH 3/4] Add try() to manage opened files/rsrcs --- app/src/processing/app/Util.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/processing/app/Util.java b/app/src/processing/app/Util.java index 4c94af5fe..9c48138c7 100644 --- a/app/src/processing/app/Util.java +++ b/app/src/processing/app/Util.java @@ -688,9 +688,7 @@ static private void packageListFromFolder(File dir, String sofar, * Ignores (does not extract) any __MACOSX files from macOS archives. */ static public void unzip(File zipFile, File dest) throws IOException { - FileInputStream fis = new FileInputStream(zipFile); - CheckedInputStream checksum = new CheckedInputStream(fis, new Adler32()); - ZipInputStream zis = new ZipInputStream(new BufferedInputStream(checksum)); + try (ZipInputStream zis = new ZipInputStream( new BufferedInputStream( new CheckedInputStream( new FileInputStream(zipFile), new Adler32())))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { final String name = entry.getName(); @@ -710,6 +708,7 @@ static public void unzip(File zipFile, File dest) throws IOException { } } } + } static protected void unzipEntry(ZipInputStream zin, File f) throws IOException { From 27bcf6aa77f1aeb3aaed61d5a552587e5468eeb5 Mon Sep 17 00:00:00 2001 From: salmane Date: Tue, 17 Feb 2026 02:59:23 +0000 Subject: [PATCH 4/4] Applying try() to more rsrcs Ive also removed the test since its OS specific, and new code is supposed to be in kotlin. --- app/build.gradle.kts | 3 - app/src/processing/app/Util.java | 83 +++++++++++++-------------- app/test/processing/app/UtilTest.java | 81 -------------------------- 3 files changed, 39 insertions(+), 128 deletions(-) delete mode 100644 app/test/processing/app/UtilTest.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 741461bb4..4f91e6d98 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,9 +41,6 @@ sourceSets{ } } test{ - java { - srcDirs("test") - } kotlin{ srcDirs("test") } diff --git a/app/src/processing/app/Util.java b/app/src/processing/app/Util.java index 9c48138c7..f87a6fdce 100644 --- a/app/src/processing/app/Util.java +++ b/app/src/processing/app/Util.java @@ -60,16 +60,17 @@ static public int countLines(String what) { */ static public byte[] loadBytesRaw(File file) throws IOException { int size = (int) file.length(); - FileInputStream input = new FileInputStream(file); - byte[] buffer = new byte[size]; - int offset = 0; - int bytesRead; - while ((bytesRead = input.read(buffer, offset, size-offset)) != -1) { - offset += bytesRead; - if (bytesRead == 0) break; - } - input.close(); // weren't properly being closed - return buffer; + byte[] buffer; + try (FileInputStream input = new FileInputStream(file)) { + buffer = new byte[size]; + int offset = 0; + int bytesRead; + while ((bytesRead = input.read(buffer, offset, size - offset)) != -1) { + offset += bytesRead; + if (bytesRead == 0) break; + } + } + return buffer; } @@ -143,7 +144,7 @@ static public StringDict readSettings(String filename, String[] lines, boolean a line = line.substring(0, line.indexOf('#')).trim(); } - if (line.length() != 0 && line.charAt(0) != '#') { + if (!line.isEmpty() && line.charAt(0) != '#') { int equals = line.indexOf('='); if (equals == -1) { if (filename != null) { @@ -161,26 +162,20 @@ static public StringDict readSettings(String filename, String[] lines, boolean a } - static public void copyFile(File sourceFile, - File targetFile) throws IOException { - BufferedInputStream from = - new BufferedInputStream(new FileInputStream(sourceFile)); - BufferedOutputStream to = - new BufferedOutputStream(new FileOutputStream(targetFile)); + static public void copyFile(File sourceFile, File targetFile) throws IOException { + try ( + BufferedInputStream from = new BufferedInputStream(new FileInputStream(sourceFile)); + BufferedOutputStream to = new BufferedOutputStream(new FileOutputStream(targetFile))) { byte[] buffer = new byte[16 * 1024]; int bytesRead; while ((bytesRead = from.read(buffer)) != -1) { to.write(buffer, 0, bytesRead); } - from.close(); - - to.flush(); - to.close(); - //noinspection ResultOfMethodCallIgnored targetFile.setLastModified(sourceFile.lastModified()); //noinspection ResultOfMethodCallIgnored targetFile.setExecutable(sourceFile.canExecute()); + } } @@ -218,13 +213,15 @@ static public void saveFile(String text, File file) throws IOException { file.getAbsolutePath()); } // Could use saveStrings(), but we wouldn't be able to checkError() - PrintWriter writer = PApplet.createWriter(temp); - for (String line : lines) { - writer.println(line); - } - boolean error = writer.checkError(); // calls flush() - writer.close(); // attempt to close regardless - if (error) { + boolean error; + try (PrintWriter writer = PApplet.createWriter(temp)) { + for (String line : lines) { + writer.println(line); + } + // calls flush() + error = writer.checkError(); + } + if (error) { throw new IOException("Error while trying to save " + file); } @@ -589,7 +586,7 @@ static public StringList packageListFromClassPath(String path) { for (String piece : pieces) { //System.out.println("checking piece '" + pieces[i] + "'"); - if (piece.length() != 0) { + if (!piece.isEmpty()) { if (piece.toLowerCase().endsWith(".jar") || piece.toLowerCase().endsWith(".zip")) { //System.out.println("checking " + pieces[i]); @@ -623,8 +620,7 @@ static public StringList packageListFromClassPath(String path) { static private void packageListFromZip(String filename, StringList list) { - try { - ZipFile file = new ZipFile(filename); + try (ZipFile file = new ZipFile(filename);) { Enumeration entries = file.entries(); while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry) entries.nextElement(); @@ -643,7 +639,6 @@ static private void packageListFromZip(String filename, StringList list) { } } } - file.close(); } catch (IOException e) { System.err.println("Ignoring " + filename + " (" + e.getMessage() + ")"); //e.printStackTrace(); @@ -712,22 +707,22 @@ static public void unzip(File zipFile, File dest) throws IOException { static protected void unzipEntry(ZipInputStream zin, File f) throws IOException { - FileOutputStream out = new FileOutputStream(f); - byte[] b = new byte[512]; - int len; - while ((len = zin.read(b)) != -1) { - out.write(b, 0, len); - } - out.flush(); - out.close(); + try (FileOutputStream out = new FileOutputStream(f)) { + byte[] b = new byte[512]; + int len; + while ((len = zin.read(b)) != -1) { + out.write(b, 0, len); + } + out.flush(); + } } static public byte[] gzipEncode(byte[] what) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - GZIPOutputStream output = new GZIPOutputStream(baos); - PApplet.saveStream(output, new ByteArrayInputStream(what)); - output.close(); + try (GZIPOutputStream output = new GZIPOutputStream(baos);) { + PApplet.saveStream(output, new ByteArrayInputStream(what)); + } return baos.toByteArray(); } diff --git a/app/test/processing/app/UtilTest.java b/app/test/processing/app/UtilTest.java deleted file mode 100644 index c394d1cdf..000000000 --- a/app/test/processing/app/UtilTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package processing.app; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Assumptions; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class UtilTest { - - @Test - public void unzipLeaksFileDescriptorsOnException() throws IOException { - // thi only runs on Linux where /proc/self/fd exists otherwise skip - Assumptions.assumeTrue(new File("/proc/self/fd").exists(), - "Skipping test: /proc/self/fd not available (not Linux)"); - // create a temporary zip file here with one entry - File zipFile = File.createTempFile("leak-test", ".zip"); - zipFile.deleteOnExit(); - File destDir = File.createTempFile("dest", ""); - destDir.delete(); // turn into a directory - destDir.mkdirs(); - destDir.deleteOnExit(); - // build a simple zip file - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - ZipEntry entry = new ZipEntry("test.txt"); - zos.putNextEntry(entry); - zos.write("hello".getBytes()); - zos.closeEntry(); - } - // make the destination directory read‑only – this will cause extraction to fail - destDir.setReadOnly(); - boolean exceptionThrown = false; - try { - Util.unzip(zipFile, destDir); - } catch (IOException e) { - exceptionThrown = true; - } - assertTrue(exceptionThrown, "Expected an exception because destDir is read‑only"); - - // check if the file is open by examining /proc/self/fd symlinks - boolean fileStillOpen = isFileOpen(zipFile); - assertFalse(fileStillOpen, "File " + zipFile + " is still open after exception – leak detected"); - - destDir.setWritable(true); - destDir.delete(); - zipFile.delete(); - } - - /** - * Checks whether the given file is currently open by the current process. - * Works on Linux by reading the symlinks in /proc/self/fd. - */ - private boolean isFileOpen(File file) throws IOException { - Path fdDir = Paths.get("/proc/self/fd"); - String targetPath = file.getCanonicalPath(); - - try (Stream fdPaths = Files.list(fdDir)) { - return fdPaths - .map(Path::toFile) - .map(File::toPath) - .map(path -> { - try { - return Files.readSymbolicLink(path); - } catch (IOException e) { - return null; // not a symlink or inaccessible - } - }) - .filter(resolved -> resolved != null) - .anyMatch(resolved -> resolved.toString().equals(targetPath)); - } - } -} \ No newline at end of file