diff --git a/java/org/apache/jasper/compiler/Compiler.java b/java/org/apache/jasper/compiler/Compiler.java index 69c6605f177e..e43c13414c09 100644 --- a/java/org/apache/jasper/compiler/Compiler.java +++ b/java/org/apache/jasper/compiler/Compiler.java @@ -37,6 +37,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.Jar; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; import org.apache.tomcat.util.scan.JarFactory; /** @@ -474,7 +475,40 @@ public boolean isOutDated(boolean checkClass) { String key = include.getKey(); URL includeUrl; long includeLastModified; - if (key.startsWith("jar:jar:")) { + if (key.startsWith("uri:")) { + // Key is a stable taglib URI used for TLDs in JARs outside + // the web application (avoids baking absolute paths into the + // generated code). Two forms exist: + // "uri:" – the JAR file itself + // "uri:!/" – a TLD entry within the JAR + int bangSlash = key.indexOf("!/"); + String tagUri = bangSlash < 0 + ? key.substring(4) + : key.substring(4, bangSlash); + TldCache tldCache = ctxt.getOptions().getTldCache(); + TldResourcePath tldPath = tldCache.getTldResourcePath(tagUri); + if (tldPath == null) { + return true; + } + if (bangSlash < 0) { + // JAR-level key: check the JAR file's last-modified + URLConnection urlConn = tldPath.getUrl().openConnection(); + try { + includeLastModified = urlConn.getLastModified(); + } finally { + urlConn.getInputStream().close(); + } + } else { + // TLD-entry key: check the entry's last-modified within the JAR + String entryName = key.substring(bangSlash + 2); + try (Jar jar = tldPath.openJar()) { + if (jar == null) { + return true; + } + includeLastModified = jar.getLastModified(entryName); + } + } + } else if (key.startsWith("jar:jar:")) { // Assume we constructed this correctly int entryStart = key.lastIndexOf("!/"); String entry = key.substring(entryStart + 2); diff --git a/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java b/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java index 8bde741fd828..dab1355464b4 100644 --- a/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java +++ b/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java @@ -133,7 +133,10 @@ public String toString() { } if (jar != null) { if (path == null) { - // JAR not in the web application so add it directly + // JAR not in the web application so add it directly. Use the + // stable taglib URI as the dependency key instead of the + // absolute JAR URL to keep the generated servlet code + // deterministic across build environments. URL jarUrl = jar.getJarFileURL(); long lastMod; URLConnection urlConn = null; @@ -151,12 +154,19 @@ public String toString() { } } } - pageInfo.addDependant(jarUrl.toExternalForm(), Long.valueOf(lastMod)); + pageInfo.addDependant("uri:" + uriIn, Long.valueOf(lastMod)); } - // Add TLD within the JAR to the dependency list + // Add TLD within the JAR to the dependency list. For external + // JARs (path == null) use a stable "uri:...!/entryName" key + // instead of the absolute jar.getURL(entryName) to keep the + // generated servlet code deterministic across build environments. String entryName = tldResourcePath.getEntryName(); try { - pageInfo.addDependant(jar.getURL(entryName), Long.valueOf(jar.getLastModified(entryName))); + String tldKey = path != null + ? jar.getURL(entryName) + : "uri:" + uriIn + "!/" + entryName; + pageInfo.addDependant(tldKey, + Long.valueOf(jar.getLastModified(entryName))); } catch (IOException ioe) { throw new JasperException(ioe); } diff --git a/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java b/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java index 901226269653..a9caf237facb 100644 --- a/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java +++ b/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java @@ -16,13 +16,29 @@ */ package org.apache.jasper.compiler; +import java.io.File; +import java.io.FileOutputStream; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + import jakarta.servlet.http.HttpServletResponse; import org.junit.Assert; import org.junit.Test; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.servlet.JspServlet; import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.scan.StandardJarScanFilter; +import org.apache.tomcat.util.scan.StandardJarScanner; /** * Test case for {@link TagLibraryInfoImpl}. @@ -39,7 +55,6 @@ public void testRelativeTldLocation() throws Exception { Assert.assertEquals(HttpServletResponse.SC_OK, rc); } - /* * https://bz.apache.org/bugzilla/show_bug.cgi?id=64373 */ @@ -53,4 +68,107 @@ public void testTldFromExplodedWar() throws Exception { Assert.assertEquals(HttpServletResponse.SC_OK, rc); } + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=70001 + * + * Verify that taglib directives referencing a TLD in a JAR that is outside + * the web application (i.e. on the classpath but not in WEB-INF/lib) produce + * a stable, environment-independent key in the generated servlet's + * {@code _jspx_dependants} map. + * + * Before the fix, the key was an absolute {@code jar:file:/...} URL that + * encoded the build-environment-specific JAR location, making JSP compilation + * non-deterministic. After the fix the key must use the {@code "uri:"} prefix + * followed by the taglib URI from the JSP directive. + */ + @Test + public void testExternalTaglibDependantUsesUri() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File appDir = new File("test/webapp"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + StandardJarScanner scanner = (StandardJarScanner) ctx.getJarScanner(); + StandardJarScanFilter filter = (StandardJarScanFilter) scanner.getJarScanFilter(); + filter.setTldSkip(filter.getTldSkip() + ",testclasses"); + filter.setPluggabilitySkip(filter.getPluggabilitySkip() + ",testclasses"); + + // Add a JAR containing the test TLD to the *parent* classloader rather + // than to WEB-INF/lib. The TLD scanner then sees it as an external JAR + // (TldResourcePath.getWebappPath() == null), which is the code path that + // the fix for non-deterministic _jspx_dependants addresses. + File jar = createExternalTaglibJar(); + ClassLoader parent = Thread.currentThread().getContextClassLoader(); + ctx.setParentClassLoader(new URLClassLoader(new URL[] { jar.toURI().toURL() }, parent)); + + tomcat.start(); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + + "/test/jsp/generator/external-taglib.jsp", body, null); + Assert.assertEquals(body.toString(), HttpServletResponse.SC_OK, rc); + + // Retrieve the _jspx_dependants map from the compiled servlet via the + // JspServletWrapper. + Context webCtx = (Context) tomcat.getHost().findChild("/test"); + Wrapper jspWrapper = (Wrapper) webCtx.findChild("jsp"); + JspServlet jspServlet = (JspServlet) jspWrapper.getServlet(); + Field rctxtField = JspServlet.class.getDeclaredField("rctxt"); + rctxtField.setAccessible(true); + JspRuntimeContext rctxt = (JspRuntimeContext) rctxtField.get(jspServlet); + Map dependants = rctxt.getWrapper( + "/jsp/generator/external-taglib.jsp").getDependants(); + + Assert.assertNotNull("Expected non-null _jspx_dependants map", dependants); + + // No key in _jspx_dependants should be an absolute file/jar URL. + // Such URLs embed environment-specific paths and make JSP compilation + // non-deterministic. + for (String key : dependants.keySet()) { + Assert.assertFalse( + "_jspx_dependants must not contain absolute paths for external taglib JARs, got: " + key, + key.startsWith("jar:file:") || key.startsWith("file:")); + } + + // The external taglib JAR and its TLD entry must each be recorded with + // a stable "uri:" key rather than an absolute path. + Assert.assertTrue( + "Expected 'uri:http://tomcat.apache.org/test/external-taglib' key in _jspx_dependants", + dependants.containsKey("uri:http://tomcat.apache.org/test/external-taglib")); + Assert.assertTrue( + "Expected 'uri:http://tomcat.apache.org/test/external-taglib!/META-INF/external-taglib-test.tld'" + + " key in _jspx_dependants", + dependants.containsKey( + "uri:http://tomcat.apache.org/test/external-taglib!/META-INF/external-taglib-test.tld")); + } + + /** + * Creates a temporary JAR containing a minimal TLD with URI + * {@code http://tomcat.apache.org/test/external-taglib}. The TLD has no + * validator and no tag-handler classes so the JAR itself is the only + * dependency required to compile a JSP that references it. + */ + private static File createExternalTaglibJar() throws Exception { + String tld = + "\n" + + "\n" + + " 1.0\n" + + " ext\n" + + " http://tomcat.apache.org/test/external-taglib\n" + + "\n"; + + File jar = File.createTempFile("external-taglib-test", ".jar"); + jar.deleteOnExit(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar))) { + jos.putNextEntry(new JarEntry("META-INF/external-taglib-test.tld")); + jos.write(tld.getBytes(StandardCharsets.UTF_8)); + jos.closeEntry(); + } + + return jar; + } } diff --git a/test/webapp/jsp/generator/external-taglib.jsp b/test/webapp/jsp/generator/external-taglib.jsp new file mode 100644 index 000000000000..b2a6936ce0be --- /dev/null +++ b/test/webapp/jsp/generator/external-taglib.jsp @@ -0,0 +1,19 @@ +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%> +<%@ page contentType="text/plain" %> +<%@ taglib prefix="ext" uri="http://tomcat.apache.org/test/external-taglib" %> +OK