Close the JarFile when JarAnalyzer's constructor fails - #166
Conversation
The constructor opened the JarFile and then listed and sorted its entries before reading the manifest. Only the manifest read was guarded: if listing or sorting threw, the constructor exited with the JarFile still open, and the caller had no reference on which to call closeQuietly(), so the handle leaked. Comparator.comparing(ZipEntry::getName) is one way in - it throws a NullPointerException on an entry with a null name, which a malformed archive can carry. Wrap everything between opening the file and assigning jarData, and release the handle on any IOException or RuntimeException before rethrowing. This also folds in the existing manifest cleanup, so there is a single exit path rather than two. Fixes apache#139
|
|
||
| // Sorting of list is done by name to ensure a bytecode hash is always consistent. | ||
| entries.sort(Comparator.comparing(ZipEntry::getName)); | ||
| // Sorting of list is done by name to ensure a bytecode hash is always consistent. |
There was a problem hiding this comment.
Sorting of list is done by name --> Sort list by name
| Manifest manifest = jarFile.getManifest(); | ||
|
|
||
| this.jarData = new JarData(file, manifest, entries); | ||
| } catch (IOException | RuntimeException e) { |
There was a problem hiding this comment.
don't catch raw RuntimeException
| List<JarEntry> entries = Collections.list(jarFile.entries()); | ||
| try { | ||
| // Obtain entries list. | ||
| List<JarEntry> entries = Collections.list(jarFile.entries()); |
There was a problem hiding this comment.
can this throw? why is it in the try block?
|
All three addressed, and the middle one changed the shape of the fix for the better. Catching On the entries list and the sort being inside the try: they are still in it, but the block is no longer a catch, so the question changes. Nothing in those two lines throws anything worth handling. They sit inside the Comment reworded. |
elharo
left a comment
There was a problem hiding this comment.
Your try block isn't big enough. It should cover mist of the constructor to make finally work
Fixes #139
Problem
JarAnalyzer's constructor opens theJarFile, then lists and sorts its entries, and only afterwards reads the manifest:Only the manifest read is guarded. If the listing or the sort throws, the constructor exits with the
JarFilestill open — and because construction failed, the caller has no instance on which to callcloseQuietly(), so the handle leaks. The class javadoc already promises the file "will be closed if this occurs".Comparator.comparing(ZipEntry::getName)is one way in: it throwsNullPointerExceptionon an entry with a null name, which a malformed archive can carry.Fix
Wrap everything between opening the file and assigning
jarData, and release the handle on anyIOExceptionorRuntimeExceptionbefore rethrowing. That covers the sort, the entry listing, and anything else added to this region later, rather than enumerating individual failure modes.It also folds in the manifest cleanup that was already there, so there is one exit path instead of two — net effect is +11/-9 in a single method.
Note on #148
This is adjacent to your #148, which removes the
ZipExceptionre-wrap in the block immediately above. The two are semantically independent — that one changes how the open fails, this one changes what happens after a successful open — so they compose cleanly. Happy to rebase on top of it if it lands first, or to fold both into one change if you'd prefer.Testing
mvn test— 78 tests across the module, all passing.I did not add a test for the leak itself. Reproducing it needs an entry with a null name, which standard tooling won't produce, and asserting the descriptor was released needs the
JarFileto be injectable — the class constructs it internally. On POSIX a "can the file be deleted afterwards" assertion passes whether or not the handle leaked, so it would be a test that only means something on Windows. I'd rather not add a misleading one, but if you want a specific approach here I'm happy to write it.