1+ package processing .app ;
2+
3+ import org .junit .jupiter .api .Test ;
4+ import org .junit .jupiter .api .Assumptions ;
5+ import java .io .File ;
6+ import java .io .FileOutputStream ;
7+ import java .io .IOException ;
8+ import java .nio .file .Files ;
9+ import java .nio .file .Path ;
10+ import java .nio .file .Paths ;
11+ import java .util .stream .Stream ;
12+ import java .util .zip .ZipEntry ;
13+ import java .util .zip .ZipOutputStream ;
14+
15+ import static org .junit .jupiter .api .Assertions .assertFalse ;
16+ import static org .junit .jupiter .api .Assertions .assertTrue ;
17+
18+ public class UtilTest {
19+
20+ @ Test
21+ public void unzipLeaksFileDescriptorsOnException () throws IOException {
22+ // thi only runs on Linux where /proc/self/fd exists otherwise skip
23+ Assumptions .assumeTrue (new File ("/proc/self/fd" ).exists (),
24+ "Skipping test: /proc/self/fd not available (not Linux)" );
25+ // create a temporary zip file here with one entry
26+ File zipFile = File .createTempFile ("leak-test" , ".zip" );
27+ zipFile .deleteOnExit ();
28+ File destDir = File .createTempFile ("dest" , "" );
29+ destDir .delete (); // turn into a directory
30+ destDir .mkdirs ();
31+ destDir .deleteOnExit ();
32+ // build a simple zip file
33+ try (ZipOutputStream zos = new ZipOutputStream (new FileOutputStream (zipFile ))) {
34+ ZipEntry entry = new ZipEntry ("test.txt" );
35+ zos .putNextEntry (entry );
36+ zos .write ("hello" .getBytes ());
37+ zos .closeEntry ();
38+ }
39+ // make the destination directory read‑only – this will cause extraction to fail
40+ destDir .setReadOnly ();
41+ boolean exceptionThrown = false ;
42+ try {
43+ Util .unzip (zipFile , destDir );
44+ } catch (IOException e ) {
45+ exceptionThrown = true ;
46+ }
47+ assertTrue (exceptionThrown , "Expected an exception because destDir is read‑only" );
48+
49+ // check if the file is open by examining /proc/self/fd symlinks
50+ boolean fileStillOpen = isFileOpen (zipFile );
51+ assertFalse (fileStillOpen , "File " + zipFile + " is still open after exception – leak detected" );
52+
53+ destDir .setWritable (true );
54+ destDir .delete ();
55+ zipFile .delete ();
56+ }
57+
58+ /**
59+ * Checks whether the given file is currently open by the current process.
60+ * Works on Linux by reading the symlinks in /proc/self/fd.
61+ */
62+ private boolean isFileOpen (File file ) throws IOException {
63+ Path fdDir = Paths .get ("/proc/self/fd" );
64+ String targetPath = file .getCanonicalPath ();
65+
66+ try (Stream <Path > fdPaths = Files .list (fdDir )) {
67+ return fdPaths
68+ .map (Path ::toFile )
69+ .map (File ::toPath )
70+ .map (path -> {
71+ try {
72+ return Files .readSymbolicLink (path );
73+ } catch (IOException e ) {
74+ return null ; // not a symlink or inaccessible
75+ }
76+ })
77+ .filter (resolved -> resolved != null )
78+ .anyMatch (resolved -> resolved .toString ().equals (targetPath ));
79+ }
80+ }
81+ }
0 commit comments