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

markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/11.0.x by this push:
     new 7813c0a98f Review "Extract the SMAP from the end of the compiled class 
file"
7813c0a98f is described below

commit 7813c0a98f7162969debfbe94989077143d511ea
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Nov 5 20:09:22 2026 +0000

    Review "Extract the SMAP from the end of the compiled class file"
    
    Completely rewrite the SMAP extract.
    Rather than replacing one hack with a (maybe) better hack, parse the
    class file as per the spec and extract the SMAP.
    
    Co-authored by: Claude / Sonnet 5.5
---
 java/org/apache/jasper/compiler/SmapUtil.java      | 193 ++++++++++-
 .../jasper/resources/LocalStrings.properties       |   1 +
 test/org/apache/jasper/compiler/TestSmapUtil.java  | 375 ++++++++++++++++++---
 3 files changed, 511 insertions(+), 58 deletions(-)

diff --git a/java/org/apache/jasper/compiler/SmapUtil.java 
b/java/org/apache/jasper/compiler/SmapUtil.java
index fb997ff06f..fa1a6c44c8 100644
--- a/java/org/apache/jasper/compiler/SmapUtil.java
+++ b/java/org/apache/jasper/compiler/SmapUtil.java
@@ -16,6 +16,9 @@
  */
 package org.apache.jasper.compiler;
 
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.EOFException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -27,8 +30,11 @@ import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.jasper.JasperException;
 import org.apache.jasper.JspCompilationContext;
@@ -49,8 +55,8 @@ 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";
+    // The SMAP data in the SourceDebugExtension attribute starts with this 
line
+    private static final String SMAP_HEADER = "SMAP\n";
 
 
     /**
@@ -436,6 +442,167 @@ public class SmapUtil {
         }
     }
 
+    /**
+     * Reads the SMAP from the SourceDebugExtension attribute of a class file. 
This walks the structure of the class
+     * file, as {@link SDEInstaller} does when it installs the attribute, so 
that exactly the bytes of the attribute are
+     * returned. Searching the class file for the content of the SMAP is not 
reliable because the same bytes may appear
+     * elsewhere in the class file, such as in a string constant, or within 
the SMAP itself.
+     * <p>
+     * The constant pool may contain more than one entry with the name of the 
attribute and other tools may add
+     * SourceDebugExtension attributes that do not contain a SMAP. Only an 
attribute that contains a SMAP, which starts
+     * with {@link SmapUtil#SMAP_HEADER}, is returned. If there is more than 
one the last is returned, since that is
+     * where {@link SDEInstaller} adds it.
+     */
+    private static class SDEReader {
+
+        private static final int MAGIC = 0xCAFEBABE;
+
+        private static final byte[] SMAP_HEADER_BYTES = 
SMAP_HEADER.getBytes(StandardCharsets.ISO_8859_1);
+
+        private final DataInputStream in;
+
+        private SDEReader(InputStream is) {
+            this.in = new DataInputStream(new BufferedInputStream(is));
+        }
+
+        /**
+         * Read the SMAP from a class file.
+         *
+         * @param is The class file
+         *
+         * @return The content of the SourceDebugExtension attribute that 
contains the SMAP, including the first line,
+         *             or {@code null} if the class file does not have such an 
attribute
+         *
+         * @throws IOException If the data is not a valid class file or 
reading it fails
+         */
+        static byte[] read(InputStream is) throws IOException {
+            return new SDEReader(is).readSDE();
+        }
+
+        private byte[] readSDE() throws IOException {
+            if (in.readInt() != MAGIC) {
+                throw new 
IOException(Localizer.getMessage("jsp.error.invalidClassFile"));
+            }
+            in.skipNBytes(2 + 2); // minor and major version
+
+            Set<Integer> sdeIndexes = readConstantPool();
+            if (sdeIndexes.isEmpty()) {
+                // Without the name in the constant pool there cannot be a 
SourceDebugExtension attribute
+                return null;
+            }
+
+            in.skipNBytes(2 + 2 + 2); // access, this, super
+            int interfaceCount = in.readUnsignedShort();
+            in.skipNBytes(interfaceCount * 2L);
+            skipMembers(); // fields
+            skipMembers(); // methods
+
+            // Class attributes
+            byte[] smap = null;
+            int attrCount = in.readUnsignedShort();
+            for (int i = 0; i < attrCount; i++) {
+                int nameIndex = in.readUnsignedShort();
+                int len = readLength();
+                if (sdeIndexes.contains(Integer.valueOf(nameIndex))) {
+                    byte[] sde = in.readNBytes(len);
+                    if (sde.length != len) {
+                        throw new EOFException();
+                    }
+                    if (isSmap(sde)) {
+                        smap = sde;
+                    }
+                } else {
+                    in.skipNBytes(len);
+                }
+            }
+            return smap;
+        }
+
+        private static boolean isSmap(byte[] sde) {
+            return sde.length >= SMAP_HEADER_BYTES.length &&
+                    Arrays.equals(sde, 0, SMAP_HEADER_BYTES.length, 
SMAP_HEADER_BYTES, 0, SMAP_HEADER_BYTES.length);
+        }
+
+        /*
+         * Skips the constant pool and returns the indexes of the Utf8 entries 
that contain the name of the
+         * SourceDebugExtension attribute. There is normally one but nothing 
prevents there being several.
+         */
+        private Set<Integer> readConstantPool() throws IOException {
+            Set<Integer> sdeIndexes = new HashSet<>();
+            int constantPoolCount = in.readUnsignedShort();
+            // Constant pool index zero is not in the class file
+            for (int i = 1; i < constantPoolCount; i++) {
+                int tag = in.readUnsignedByte();
+                switch (tag) {
+                    case 7: // Class
+                    case 8: // String
+                    case 16: // MethodType
+                    case 19: // Module
+                    case 20: // Package
+                        in.skipNBytes(2);
+                        break;
+                    case 15: // MethodHandle
+                        in.skipNBytes(3);
+                        break;
+                    case 9: // Field
+                    case 10: // Method
+                    case 11: // InterfaceMethod
+                    case 3: // Integer
+                    case 4: // Float
+                    case 12: // NameAndType
+                    case 17: // Dynamic
+                    case 18: // InvokeDynamic
+                        in.skipNBytes(4);
+                        break;
+                    case 5: // Long
+                    case 6: // Double
+                        in.skipNBytes(8);
+                        // Long and double values take two entries in the 
constant pool
+                        i++;
+                        break;
+                    case 1: // Utf8
+                        int len = in.readUnsignedShort();
+                        if (len == SDEInstaller.nameSDE.length()) {
+                            byte[] utf8 = in.readNBytes(len);
+                            if (utf8.length != len) {
+                                throw new EOFException();
+                            }
+                            if (SDEInstaller.nameSDE.equals(new String(utf8, 
StandardCharsets.UTF_8))) {
+                                sdeIndexes.add(Integer.valueOf(i));
+                            }
+                        } else {
+                            in.skipNBytes(len);
+                        }
+                        break;
+                    default:
+                        throw new 
IOException(Localizer.getMessage("jsp.error.unexpectedTag", 
Integer.valueOf(tag)));
+                }
+            }
+            return sdeIndexes;
+        }
+
+        private void skipMembers() throws IOException {
+            int count = in.readUnsignedShort();
+            for (int i = 0; i < count; i++) {
+                in.skipNBytes(2 + 2 + 2); // access, name, descriptor
+                int attrCount = in.readUnsignedShort();
+                for (int j = 0; j < attrCount; j++) {
+                    in.skipNBytes(2); // name
+                    in.skipNBytes(readLength());
+                }
+            }
+        }
+
+        private int readLength() throws IOException {
+            int len = in.readInt();
+            if (len < 0) {
+                // The length is a u4 but a valid attribute is never larger 
than the maximum size of an array
+                throw new 
IOException(Localizer.getMessage("jsp.error.invalidClassFile"));
+            }
+            return len;
+        }
+    }
+
     /**
      * Traverses the JSP AST nodes and populates the given SmapStratum with 
line mapping data.
      * @param nodes the root JSP AST nodes
@@ -769,22 +936,12 @@ public class SmapUtil {
 
             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());
+                // The SMAP is the content of the SourceDebugExtension 
attribute. It is installed as ISO-8859-1 (see
+                // installSmap()). The first line, "SMAP", is not included in 
the result.
+                byte[] sde = SDEReader.read(is);
+                if (sde != null) {
+                    return new String(sde, SMAP_HEADER.length(), sde.length - 
SMAP_HEADER.length(),
+                            StandardCharsets.ISO_8859_1);
                 }
             }
         } catch (IOException ioe) {
diff --git a/java/org/apache/jasper/resources/LocalStrings.properties 
b/java/org/apache/jasper/resources/LocalStrings.properties
index c298350b4f..02fd270277 100644
--- a/java/org/apache/jasper/resources/LocalStrings.properties
+++ b/java/org/apache/jasper/resources/LocalStrings.properties
@@ -102,6 +102,7 @@ jsp.error.invalid.tagdir=Tag file directory [{0}] does not 
start with "/WEB-INF/
 jsp.error.invalid.taglib.version=Invalid JSP version [{0}] defined by the tag 
library used by tag [{1}]
 jsp.error.invalid.varscope=Invalid variable scope [{0}]
 jsp.error.invalid.version=Invalid JSP version defined for tag file at [{0}]
+jsp.error.invalidClassFile=The data is not a valid class file
 jsp.error.ise_on_clear=Illegal to clear() when buffer size == 0
 jsp.error.java.line.number=An error occurred at line: [{0}] in the generated 
java file: [{1}]
 jsp.error.javac=Javac exception
diff --git a/test/org/apache/jasper/compiler/TestSmapUtil.java 
b/test/org/apache/jasper/compiler/TestSmapUtil.java
index 379a5751a9..85d207e679 100644
--- a/test/org/apache/jasper/compiler/TestSmapUtil.java
+++ b/test/org/apache/jasper/compiler/TestSmapUtil.java
@@ -16,78 +16,373 @@
  */
 package org.apache.jasper.compiler;
 
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
 import java.io.File;
+import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
 
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
 
+import org.apache.catalina.startup.ExpandWar;
 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";
+    // 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("test.jsp");
+
+    private final List<File> dirs = new ArrayList<>();
+    private final List<URLClassLoader> loaders = new ArrayList<>();
+
+
+    @After
+    public void cleanUp() throws IOException {
+        for (URLClassLoader loader : loaders) {
+            loader.close();
+        }
+        loaders.clear();
+        for (File dir : dirs) {
+            ExpandWar.delete(dir);
+        }
+        dirs.clear();
+    }
+
+
+    private static String smap(String fileName) {
+        return "SMAP\n" + "test_jsp.java\n" + "JSP\n" + "*S JSP\n" + "*F\n" + 
"+ 0 " + fileName + "\n" + fileName +
+                "\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));
+        ClassFile classFile = new ClassFile().sde(SMAP);
 
-        SmapStratum smapStratum = SmapUtil.loadSmap("clean", cl);
+        SmapStratum smapStratum = SmapUtil.loadSmap("Test", 
writeClass(classFile));
 
         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);
+        // 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, wherever it is in the
+        // class file.
+        ClassFile classFile = new ClassFile().constant("template text that 
ends a line with SMAP\nfake content\n")
+                .sde(SMAP).constant("more text ending with SMAP\nafter the 
name of the attribute\n");
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("Test", 
writeClass(classFile));
+
+        Assert.assertNotNull(smapStratum);
+        Assert.assertEquals(SMAP, smapStratum.toString());
+    }
+
+
+    @Test
+    public void testLoadSmapWithMarkerInSmap() throws Exception {
+        // The marker also appears within the SMAP if the name of a source 
file ends with "SMAP"
+        String smapWithMarker = smap("ERRORS.SMAP");
+        ClassFile classFile = new ClassFile().sde(smapWithMarker);
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("Test", 
writeClass(classFile));
+
+        Assert.assertNotNull(smapStratum);
+        Assert.assertEquals(smapWithMarker, smapStratum.toString());
+    }
+
+
+    @Test
+    public void testLoadSmapWithOtherAttributes() throws Exception {
+        // Attributes on the class before the SourceDebugExtension attribute, 
one of which contains the marker, and
+        // members that have attributes
+        ClassFile classFile = new 
ClassFile().wideConstants().member().classAttribute("Other", "SMAP\nnot this\n")
+                .sde(SMAP);
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("Test", 
writeClass(classFile));
 
-        URLClassLoader cl = writeClass("junk", junk);
+        Assert.assertNotNull(smapStratum);
+        Assert.assertEquals(SMAP, smapStratum.toString());
+    }
 
-        SmapStratum smapStratum = SmapUtil.loadSmap("junk", cl);
 
-        Assert.assertNotNull("SMAP extraction found the marker in the constant 
pool junk", smapStratum);
+    @Test
+    public void testLoadSmapWithMultipleDebugExtensionNames() throws Exception 
{
+        // More than one entry in the constant pool with the name of the 
attribute, one of which is for an attribute
+        // that does not contain a SMAP and that comes after the SMAP
+        ClassFile classFile = new ClassFile().sdeName().sde(SMAP).sde("not a 
SMAP\nSMAP\nfake content\n").sdeName();
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("Test", 
writeClass(classFile));
+
+        Assert.assertNotNull(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
+
+    @Test
+    public void testLoadSmapWithOtherDebugExtensionFirst() throws Exception {
+        // An attribute that does not contain a SMAP, before the one that does
+        ClassFile classFile = new ClassFile().sde("not a SMAP\nSMAP\nfake 
content\n").sde(SMAP);
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("Test", 
writeClass(classFile));
+
+        Assert.assertNotNull(smapStratum);
+        Assert.assertEquals(SMAP, smapStratum.toString());
+    }
+
+
+    @Test
+    public void testLoadSmapWithOnlyOtherDebugExtension() throws Exception {
+        // The only SourceDebugExtension attribute does not contain a SMAP
+        ClassFile classFile = new ClassFile().sde("not a SMAP\nSMAP\nfake 
content\n");
+
+        Assert.assertNull(SmapUtil.loadSmap("Test", writeClass(classFile)));
+    }
+
+
+    @Test
+    public void testLoadSmapWithMultipleSmaps() throws Exception {
+        // The last SMAP is used since that is where the SourceDebugExtension 
attribute is added
+        ClassFile classFile = new ClassFile().sde(smap("first.jsp")).sde(SMAP);
+
+        SmapStratum smapStratum = SmapUtil.loadSmap("Test", 
writeClass(classFile));
+
+        Assert.assertNotNull(smapStratum);
+        Assert.assertEquals(SMAP, smapStratum.toString());
+    }
+
+
+    @Test
+    public void testLoadSmapWithoutDebugExtension() throws Exception {
+        // The name of the attribute is present but there is no such 
attribute. The only occurrence of the marker is in
+        // a constant.
+        ClassFile classFile = new ClassFile().constant("text ending with 
SMAP\nfake content\n").sdeName();
+
+        Assert.assertNull(SmapUtil.loadSmap("Test", writeClass(classFile)));
+    }
+
+
+    @Test
+    public void testLoadSmapWithoutDebugExtensionName() throws Exception {
+        ClassFile classFile = new ClassFile().constant("text ending with 
SMAP\nfake content\n");
+
+        Assert.assertNull(SmapUtil.loadSmap("Test", writeClass(classFile)));
+    }
+
+
+    @Test
+    public void testLoadSmapNotAClassFile() throws Exception {
+        File dir = createDir();
+        Files.writeString(new File(dir, "Test.class").toPath(), "CAFEBABE 
junk\n" + SMAP, StandardCharsets.ISO_8859_1);
+
+        Assert.assertNull(SmapUtil.loadSmap("Test", loader(dir)));
+    }
+
+
+    @Test
+    public void testLoadSmapTruncatedClassFile() throws Exception {
+        byte[] bytes = new ClassFile().sde(SMAP).toBytes();
+        File dir = createDir();
+        // Truncate in the middle of the SourceDebugExtension attribute
+        Files.write(new File(dir, "Test.class").toPath(), Arrays.copyOf(bytes, 
bytes.length - 10));
+
+        Assert.assertNull(SmapUtil.loadSmap("Test", loader(dir)));
+    }
+
+
+    @Test
+    public void testInstallAndLoadSmap() throws Exception {
+        // Install the SMAP with the code that Jasper uses and then load it 
again
+        ClassFile classFile = new ClassFile().constant("text ending with 
SMAP\nfake content\n");
+        File dir = createDir();
+        File file = new File(dir, "Test.class");
+        Files.write(file.toPath(), classFile.toBytes());
+
+        SmapStratum stratum = new SmapStratum();
+        stratum.setOutputFileName("test_jsp.java");
+        stratum.setClassFileName(file.getAbsolutePath());
+        stratum.addFile("ERRORS.SMAP");
+        stratum.addLineData(1, "ERRORS.SMAP", 4, 120, 1);
+        SmapUtil.installSmap(Map.of("Test", stratum));
+
+        SmapStratum loaded = SmapUtil.loadSmap("Test", loader(dir));
+
+        Assert.assertNotNull(loaded);
+        Assert.assertEquals(stratum.getSmapString(), loaded.getSmapString());
+    }
+
+
+    private URLClassLoader writeClass(ClassFile classFile) throws Exception {
+        File dir = createDir();
+        Files.write(new File(dir, "Test.class").toPath(), classFile.toBytes());
+        return loader(dir);
+    }
+
+
+    private File createDir() {
+        File dir = new File(getTemporaryDirectory(), "smap-util-" + 
dirs.size() + "-" + System.nanoTime());
+        Assert.assertTrue(dir.mkdirs());
+        dirs.add(dir);
+        return dir;
+    }
+
+
+    private URLClassLoader loader(File dir) throws Exception {
+        URLClassLoader loader = new URLClassLoader(new URL[] { 
dir.toURI().toURL() });
+        loaders.add(loader);
+        return loader;
+    }
+
+
+    /*
+     * Builds a minimal but structurally valid class file. The constant pool 
contains the entries required by the class
+     * plus any that are added. The SourceDebugExtension attribute, if there 
is one, is the last attribute.
      */
-    private URLClassLoader writeClass(String name, byte[] data) throws 
Exception {
-        File file = new File(getTemporaryDirectory(), name + ".class");
+    private static class ClassFile {
+
+        // Constants added before the first attribute or name (constants) and 
after it (laterConstants)
+        private final List<String> constants = new ArrayList<>();
+        private final List<String> laterConstants = new ArrayList<>();
+        private final List<String[]> attributes = new ArrayList<>();
+        private boolean sdeName;
+        private boolean wide;
+        private boolean member;
+
+        ClassFile constant(String value) {
+            if (sdeName || !attributes.isEmpty()) {
+                laterConstants.add(value);
+            } else {
+                constants.add(value);
+            }
+            return this;
+        }
+
+        /*
+         * Adds the name of the SourceDebugExtension attribute to the constant 
pool without adding the attribute.
+         */
+        ClassFile sdeName() {
+            sdeName = true;
+            return this;
+        }
+
+        /*
+         * Adds a SourceDebugExtension attribute. Each attribute has its own 
entry in the constant pool for its name so
+         * adding more than one results in more than one entry with the name 
of the attribute.
+         */
+        ClassFile sde(String value) {
+            return classAttribute("SourceDebugExtension", value);
+        }
+
+        ClassFile wideConstants() {
+            wide = true;
+            return this;
+        }
+
+        ClassFile member() {
+            member = true;
+            return this;
+        }
+
+        ClassFile classAttribute(String name, String value) {
+            attributes.add(new String[] { name, value });
+            return this;
+        }
+
+        byte[] toBytes() throws IOException {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            DataOutputStream out = new DataOutputStream(baos);
+
+            // Constant pool: 1 Utf8, 2 Class, 3 Utf8, 4 Class, then the 
optional entries
+            ByteArrayOutputStream pool = new ByteArrayOutputStream();
+            DataOutputStream poolOut = new DataOutputStream(pool);
+            poolOut.writeByte(1);
+            poolOut.writeUTF("Test");
+            poolOut.writeByte(7);
+            poolOut.writeShort(1);
+            poolOut.writeByte(1);
+            poolOut.writeUTF("java/lang/Object");
+            poolOut.writeByte(7);
+            poolOut.writeShort(3);
+            int count = 5;
 
-        String classData = new String(data, StandardCharsets.ISO_8859_1) + 
SMAP;
-        Files.writeString(file.toPath(), classData, 
StandardCharsets.ISO_8859_1);
+            for (String constant : constants) {
+                poolOut.writeByte(1);
+                poolOut.writeUTF(constant);
+                count++;
+            }
+            if (sdeName) {
+                poolOut.writeByte(1);
+                poolOut.writeUTF("SourceDebugExtension");
+                count++;
+            }
+            int[] attributeNameIndexes = new int[attributes.size()];
+            for (int i = 0; i < attributes.size(); i++) {
+                poolOut.writeByte(1);
+                poolOut.writeUTF(attributes.get(i)[0]);
+                attributeNameIndexes[i] = count++;
+            }
+            if (wide) {
+                // Long and double take two entries in the constant pool
+                poolOut.writeByte(5);
+                poolOut.writeLong(1L);
+                count += 2;
+                poolOut.writeByte(6);
+                poolOut.writeDouble(1.0d);
+                count += 2;
+            }
+            for (String constant : laterConstants) {
+                poolOut.writeByte(1);
+                poolOut.writeUTF(constant);
+                count++;
+            }
+            // Name and descriptor of the member
+            int memberNameIndex = 1;
 
-        return new URLClassLoader(new URL[] { 
file.getParentFile().toURI().toURL() });
+            out.writeInt(0xCAFEBABE);
+            out.writeShort(0); // minor
+            out.writeShort(65); // major
+            out.writeShort(count);
+            out.write(pool.toByteArray());
+            out.writeShort(0x21); // access
+            out.writeShort(2); // this
+            out.writeShort(4); // super
+            out.writeShort(0); // interfaces
+            out.writeShort(0); // fields
+            if (member) {
+                // One method with one attribute that contains the marker
+                out.writeShort(1);
+                out.writeShort(0x1); // access
+                out.writeShort(memberNameIndex);
+                out.writeShort(memberNameIndex);
+                out.writeShort(1);
+                out.writeShort(memberNameIndex);
+                byte[] data = "SMAP\nin a 
member\n".getBytes(StandardCharsets.ISO_8859_1);
+                out.writeInt(data.length);
+                out.write(data);
+            } else {
+                out.writeShort(0);
+            }
+            out.writeShort(attributes.size());
+            for (int i = 0; i < attributes.size(); i++) {
+                out.writeShort(attributeNameIndexes[i]);
+                byte[] data = 
attributes.get(i)[1].getBytes(StandardCharsets.ISO_8859_1);
+                out.writeInt(data.length);
+                out.write(data);
+            }
+            out.flush();
+            return baos.toByteArray();
+        }
     }
 }


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

Reply via email to