Skip to content

fix: stop the install-root search from adopting unrelated directories - #7704

Open
Vest wants to merge 1 commit into
PCGen:masterfrom
Vest:fix/install-root-scan
Open

fix: stop the install-root search from adopting unrelated directories#7704
Vest wants to merge 1 commit into
PCGen:masterfrom
Vest:fix/install-root-scan

Conversation

@Vest

@Vest Vest commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Problem

ConfigurationSettings.findInstallRoot locates PCGen's bundled data by climbing from java.home. It climbed to the filesystem root and, at every ancestor, tested every child directory for a data + system pair:

return Stream.iterate(Path.of(javaHome), Objects::nonNull, Path::getParent)
        .flatMap(dir -> Stream.concat(Stream.of(dir), listDirectories(dir)))
        .filter(ConfigurationSettings::looksLikeInstallRoot)
        .findFirst();

Any unrelated directory holding both markers is adopted as the install root.

On a normal Linux install java.home is something like /usr/lib/jvm/<jdk>, so the walk scans the children of /usr and /. A stray match there would silently repoint PCGen's bundled data. The walk also re-ran on every lookup (see below).

The same flaw makes two unit tests environment-dependent:

  • findInstallRoot_should_beEmpty_when_noDataFound
  • findInstallRoot_should_requireBothMarkers_when_onlyDataPresent

Both use @TempDir, which on Linux lives under /tmp. Any PCGen checkout elsewhere in /tmp has data/ and system/ at its root, is found as a sibling, and both "should be empty" assertions fail — unrelated to the code under test.

Causation confirmed on an unpatched checkout, with only /tmp differing between runs:

/tmp contents Result
no data+system directory 14 tests, 0 failures
plus an empty /tmp/decoy/{data,system} 14 tests, 2 failures (exactly those two)

Changes

  • Match the jpackage layout only. The search now considers an ancestor itself, or that ancestor's app subdirectory — the directory jpackage creates beside runtime — rather than every child of every ancestor. All existing layouts (macOS bundle, Linux image, Windows image, data above java.home) still resolve.
  • Bound the climb, so it cannot reach /usr or / from a deeply nested java.home.
  • Canonicalize the starting path. Path.getParent() is purely lexical, so a symlinked java.home yielded the link's textual ancestors instead of the real ones. Falls back to the normalized absolute path if the real path cannot be resolved.
  • Memoise the result. java.home is fixed for the life of the JVM, but every @-prefixed path expansion called getInstallRoot() and repeated the filesystem walk.

Tests

Four regression tests added to ConfigurationSettingsTest (14 -> 18 tests):

  • an arbitrary sibling directory is ignored
  • app is preferred over a decoy sibling
  • a symlinked java.home resolves correctly
  • the depth bound holds

Verified on macOS (full unit suite: 17,158 tests, 0 failures) and on Linux, where the previously failing scenario was reproduced with the decoy directory deliberately in place and now reports 18 tests, 0 failures.

Checkstyle output is unchanged; the remaining violations are pre-existing unused imports in files this PR does not touch.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

findInstallRoot climbed from java.home to the filesystem root and, at
every ancestor, tested every child directory for a data + system pair.
Any unrelated directory that happened to hold both was adopted as the
install root, so a checkout left under /tmp hijacked the result and made
the two negative findInstallRoot tests fail for reasons that had nothing
to do with the code under test. On a normal install the walk also reached
/usr and /, where a stray match would silently repoint PCGen's bundled
data.

The search now only accepts an ancestor itself or that ancestor's "app"
subdirectory, which is what jpackage creates beside "runtime", and the
climb is bounded so it cannot reach /usr or /. All four packaged layouts
still resolve.

The starting path is canonicalized as well. Path.getParent is purely
lexical, so a symlinked java.home previously yielded the link's textual
ancestors rather than the real ones.

The result is memoised: java.home is fixed for the life of the JVM, while
every @-prefixed path expansion asked for the install root and re-ran the
filesystem walk.

Adds regression tests for the unrelated-sibling case, the app-versus-decoy
preference, a symlinked java.home and the depth bound.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Vest Vest self-assigned this Aug 17, 2026
@Vest Vest added the bug label Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant