This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 5cf3347e2ae959fa98891bd92320694a99a697a6
Author: opencode <[email protected]>
AuthorDate: Sun Oct 18 23:45:40 2026 +0200

    Extract the SMAP from the end of the compiled class file
    
    SmapUtil.getSmap() scanned the class file from the start for the byte
    sequence "SMAP\n" and treated everything after the first occurrence as
    the SMAP. A compiled JSP servlet class can legitimately contain that
    byte sequence earlier, for example in a string constant in the constant
    pool created from template text that has the word "SMAP" at the end of
    a line. In that case the extraction returned garbage from the wrong
    offset, silently corrupting the JSP line mapping used to report errors.
    
    The SourceDebugExtension attribute that holds the SMAP is appended as
    the last attribute of the class file by SDEInstaller and, as generated
    by SmapStratum, it contains the marker only once, so take the SMAP from
    the last occurrence of the marker.
    
    Add tests that verify extraction from a class file where the marker
    only appears in the simulated SourceDebugExtension attribute and from
    one where an earlier occurrence is present in the simulated constant
    pool.
---
 java/org/apache/jasper/compiler/SmapUtil.java     | 66 ++++++----------
 test/org/apache/jasper/compiler/TestSmapUtil.java | 93 +++++++++++++++++++++++
 2 files changed, 116 insertions(+), 43 deletions(-)

diff --git a/java/org/apache/jasper/compiler/SmapUtil.java 
b/java/org/apache/jasper/compiler/SmapUtil.java
index ce6b2ecaef..50424f8fba 100644
--- a/java/org/apache/jasper/compiler/SmapUtil.java
+++ b/java/org/apache/jasper/compiler/SmapUtil.java
@@ -16,7 +16,6 @@
  */
 package org.apache.jasper.compiler;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -50,6 +49,9 @@ public class SmapUtil {
 
     private static final Charset SMAP_ENCODING = StandardCharsets.UTF_8;
 
+    // Marker that begins the SMAP data in the SourceDebugExtension attribute
+    private static final String SMAP_START_MARKER = "SMAP\n";
+
 
     /**
      * Generates an appropriate SMAP representing the current compilation 
context. (JSR-045.)
@@ -763,55 +765,33 @@ public class SmapUtil {
     }
 
 
-    @SuppressWarnings("null") // is cannot be null when used
     private static String getSmap(String className, ClassLoader cl) {
-        Charset encoding = StandardCharsets.ISO_8859_1;
-        boolean found = false;
-        String smap = null;
-
         InputStream is = null;
         try {
             is = cl.getResourceAsStream(className.replace(".", "/") + ".smap");
             if (is != null) {
-                encoding = SMAP_ENCODING;
-                found = true;
-            } else {
-                is = cl.getResourceAsStream(className.replace(".", "/") + 
".class");
-                if (is != null) {
-                    // Alternative approach would be to read the class file as 
per the
-                    // JLS. That would require duplicating a lot of BCEL 
functionality.
-                    int b = is.read();
-                    while (b != -1) {
-                        if (b == 'S') {
-                            if ((b = is.read()) != 'M') {
-                                continue;
-                            }
-                            if ((b = is.read()) != 'A') {
-                                continue;
-                            }
-                            if ((b = is.read()) != 'P') {
-                                continue;
-                            }
-                            if ((b = is.read()) != '\n') {
-                                continue;
-                            }
-                            found = true;
-                            break;
-                        }
-                        b = is.read();
-                    }
-                }
+                return new String(is.readAllBytes(), SMAP_ENCODING);
             }
 
-            if (found) {
-                ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
-                byte[] buf = new byte[1024];
-                int numRead;
-                while ((numRead = is.read(buf)) >= 0) {
-                    baos.write(buf, 0, numRead);
+            is = cl.getResourceAsStream(className.replace(".", "/") + 
".class");
+            if (is != null) {
+                // The SourceDebugExtension attribute that contains the SMAP
+                // is appended as the last attribute of the class file (see
+                // SDEInstaller) so the SMAP to extract is the one that starts
+                // at the last occurrence of the marker. The first occurrence
+                // cannot be used because the same byte sequence may appear
+                // earlier in the class file, for example in a string constant
+                // in the constant pool created from the template text of the
+                // page. Note that, unlike SmapStratum, the optional JSR-045
+                // terminator line "* END SMAP" also contains the marker. The
+                // SMAP data is installed as ISO-8859-1 (see installSmap())
+                // which preserves every byte of the class file, so the marker
+                // cannot be missed by the search below.
+                String classData = new String(is.readAllBytes(), 
StandardCharsets.ISO_8859_1);
+                int index = classData.lastIndexOf(SMAP_START_MARKER);
+                if (index >= 0) {
+                    return classData.substring(index + 
SMAP_START_MARKER.length());
                 }
-
-                smap = new String(baos.toByteArray(), encoding);
             }
         } catch (IOException ioe) {
             Log log = LogFactory.getLog(SmapUtil.class);
@@ -826,6 +806,6 @@ public class SmapUtil {
                 }
             }
         }
-        return smap;
+        return null;
     }
 }
diff --git a/test/org/apache/jasper/compiler/TestSmapUtil.java 
b/test/org/apache/jasper/compiler/TestSmapUtil.java
new file mode 100644
index 0000000000..379a5751a9
--- /dev/null
+++ b/test/org/apache/jasper/compiler/TestSmapUtil.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.jasper.compiler;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.startup.TomcatBaseTest;
+
+public class TestSmapUtil extends TomcatBaseTest {
+
+    // The SMAP of a Jasper generated class. The content is valid for
+    // loadSmap() and follows the format generated by SmapStratum.
+    private static final String SMAP = "SMAP\n" +
+            "test.jsp\n" +
+            "JSP\n" +
+            "*S JSP\n" +
+            "*F\n" +
+            "+ 0 test.jsp\n" +
+            "test.jsp\n" +
+            "*L\n" +
+            "1,4:120,0\n" +
+            "*E\n";
+
+    @Test
+    public void testLoadSmapFromDebugExtension() throws Exception {
+        // A class file that only contains the SMAP in the (simulated)
+        // SourceDebugExtension attribute at the end of the file
+        URLClassLoader cl = writeClass("clean", "CAFEBABE 
junk".getBytes(StandardCharsets.ISO_8859_1));
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("clean", cl);
+
+        Assert.assertNotNull(smapStratum);
+        Assert.assertEquals(SMAP, smapStratum.toString());
+    }
+
+    @Test
+    public void testLoadSmapWithMarkerInConstantPool() throws Exception {
+        // Jasper does not read the class file as described by the JLS so a
+        // string constant in the constant pool that contains "SMAP\n" (which
+        // a page with the word "SMAP" at the end of a line of template text
+        // produces) must not be mistaken for the start of the SMAP. The SMAP
+        // of the SourceDebugExtension attribute has to be extracted even
+        // though an earlier occurrence of the marker is present.
+        byte[] junk =
+                "CAFEBABE constant pool junk that ends a line of template text 
with SMAP\nfake content\n"
+                        .getBytes(StandardCharsets.ISO_8859_1);
+
+        URLClassLoader cl = writeClass("junk", junk);
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("junk", cl);
+
+        Assert.assertNotNull("SMAP extraction found the marker in the constant 
pool junk", smapStratum);
+        Assert.assertEquals(SMAP, smapStratum.toString());
+    }
+
+    /**
+     * Writes a file named [name].class to the test temporary directory. The
+     * content simulates the end of a compiled JSP class file: the given data
+     * followed by the SMAP, since Jasper places the SourceDebugExtension
+     * attribute last and loadSmap() does not parse the class file structure.
+     *
+     * @return a class loader for the directory containing the file
+     */
+    private URLClassLoader writeClass(String name, byte[] data) throws 
Exception {
+        File file = new File(getTemporaryDirectory(), name + ".class");
+
+        String classData = new String(data, StandardCharsets.ISO_8859_1) + 
SMAP;
+        Files.writeString(file.toPath(), classData, 
StandardCharsets.ISO_8859_1);
+
+        return new URLClassLoader(new URL[] { 
file.getParentFile().toURI().toURL() });
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to