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)
+    }
 }

Reply via email to