This is an automated email from the ASF dual-hosted git repository. paulk-asert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 17b9e5e0d6a63b6f547d3caa460b72bd99f1a021 Author: Paul King <[email protected]> AuthorDate: Tue May 12 08:40:39 2026 +1000 GROOVY-12005: Harden Grape against cache corruption and CDN throttling (cont'd) --- .../main/groovy/groovy/grape/ivy/GrapeIvy.groovy | 35 ++++++++++++++++++++-- .../grape/ivy/GrapeConfigResolutionTest.groovy | 35 ++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy/GrapeIvy.groovy b/subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy/GrapeIvy.groovy index df3345515c..827c33f872 100644 --- a/subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy/GrapeIvy.groovy +++ b/subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy/GrapeIvy.groovy @@ -141,11 +141,11 @@ class GrapeIvy implements GrapeEngine { URL defaultConfig = GrapeIvy.getResource('defaultGrapeConfig.xml') URL effective = resolveGrapeConfigUrl() ?: defaultConfig try { - settings.load(effective) + loadIvySettings(effective) } catch (ParseException e) { LOGGER.log(Level.WARNING, "Ivy config '${effective}' appears corrupt - ignoring and using default config. Error: ${e.message}", e) - settings.load(defaultConfig) + loadIvySettings(defaultConfig) } settings.setDefaultCache(getGrapeCacheDir()) settings.setVariable('ivy.default.configuration.m2compatible', 'true') @@ -292,6 +292,37 @@ class GrapeIvy implements GrapeEngine { return f.exists() ? f.toURI().toURL() : null } + /** + * Loads the given URL into the Ivy settings. For {@code file:} URLs backed + * by a real file we route through the {@code load(File)} overload, because + * {@code IvySettings.load(File)} sets {@code ivy.settings.dir} to a filesystem + * path while {@code IvySettings.load(URL)} sets it to a {@code file:}-prefixed + * URL string. User-supplied ivysettings.xml files commonly interpolate + * {@code ${ivy.settings.dir}} into resolver patterns expecting a path + * (e.g. {@code <filesystem>} {@code ivy} / {@code artifact} patterns), so + * the URL form silently breaks those resolvers. Classpath / jar / http + * resources go through the URL overload since {@code ivy.settings.dir} + * isn't meaningful as a filesystem path for them anyway. + */ + private void loadIvySettings(URL url) throws ParseException, IOException { + File asFile = urlAsLocalFile(url) + if (asFile != null) { + settings.load(asFile) + } else { + settings.load(url) + } + } + + @PackageScope static File urlAsLocalFile(URL url) { + if (url == null || url.protocol != 'file') return null + try { + File f = new File(url.toURI()) + return f.isFile() ? f : null + } catch (Exception ignored) { + return null + } + } + /** * Chooses the target class loader for a grab operation. * diff --git a/subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy/GrapeConfigResolutionTest.groovy b/subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy/GrapeConfigResolutionTest.groovy index d7b8aa95f2..16a50d1680 100644 --- a/subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy/GrapeConfigResolutionTest.groovy +++ b/subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy/GrapeConfigResolutionTest.groovy @@ -71,4 +71,39 @@ final class GrapeConfigResolutionTest { void returns_null_for_missing_filesystem_path() { assertNull GrapeIvy.resolveExplicitConfig('/nonexistent/no/such/file-grape.xml') } + + @Test + void urlAsLocalFile_returns_file_for_existing_file_url(@TempDir File tmp) { + // GROOVY-8372 / testConf2 regression: IvySettings.load(File) and load(URL) + // set ivy.settings.dir differently (filesystem path vs file:-prefixed URL + // string). User-supplied ivysettings.xml files that interpolate + // ${ivy.settings.dir} into resolver patterns expect the path form, so we + // route file:-URLs back through the File overload via urlAsLocalFile. + File f = new File(tmp, 'custom-grape.xml') + f << '<ivysettings/>' + File mapped = GrapeIvy.urlAsLocalFile(f.toURI().toURL()) + assertEquals f, mapped + } + + @Test + void urlAsLocalFile_returns_null_for_non_file_url() { + // e.g. a jar:file:... or http:... URL — only file: URLs can be routed + // through the File overload. (jar: URLs are what classpath resources + // resolve to in production; in tests they often resolve to real files + // under build/resources/main/, which is a file: URL backed by a real + // file, so that case is covered by the "existing file url" test above.) + URL jarUrl = new URL('jar:file:/tmp/whatever.jar!/groovy/grape/ivy/foo.xml') + assertNull GrapeIvy.urlAsLocalFile(jarUrl) + } + + @Test + void urlAsLocalFile_returns_null_for_nonexistent_file_url() { + URL bogus = new File('/nonexistent/no/such/file-grape.xml').toURI().toURL() + assertNull GrapeIvy.urlAsLocalFile(bogus) + } + + @Test + void urlAsLocalFile_returns_null_for_null_input() { + assertNull GrapeIvy.urlAsLocalFile(null) + } }
