001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.bcel.classfile;
020
021import java.io.BufferedInputStream;
022import java.io.DataInput;
023import java.io.DataInputStream;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.util.zip.ZipEntry;
028import java.util.zip.ZipFile;
029
030import org.apache.bcel.Const;
031import org.apache.commons.io.IOUtils;
032
033/**
034 * Wrapper class that parses a given Java .class file. The method <a href ="#parse">parse</a> returns a
035 * <a href ="JavaClass.html"> JavaClass</a> object on success. When an I/O error or an inconsistency occurs an
036 * appropriate exception is propagated back to the caller.
037 *
038 * The structure and the names comply, except for a few conveniences, exactly with the
039 * <a href="https://docs.oracle.com/javase/specs/"> JVM specification 1.0</a>. See this paper for further details about
040 * the structure of a bytecode file.
041 */
042public final class ClassParser {
043
044    private static final int BUFSIZE = 8192;
045
046    static int[] readU2U2Table(final DataInput dataInput) throws IOException {
047        final int count = dataInput.readUnsignedShort();
048        final int[] table = new int[count];
049        for (int i = 0; i < count; i++) {
050            table[i] = dataInput.readUnsignedShort();
051        }
052        return table;
053    }
054
055    private DataInputStream dataInputStream;
056    private final boolean fileOwned;
057    private final String fileName;
058    private String zipFile;
059    private int classNameIndex;
060    private int superclassNameIndex;
061    private int major; // Compiler version
062    private int minor; // Compiler version
063    // Actual Code layout selected after checking both the explicit option and the class file version.
064    private boolean compactCodeHeader;
065    // Versions 45.0 through 45.2 can also use standard Code headers, so the narrower layout requires an explicit option.
066    private boolean compactCodeHeaderEnabled;
067    private int accessFlags; // Access rights of parsed class
068    private int[] interfaces; // Names of implemented interfaces
069    private ConstantPool constantPool; // collection of constants
070    private Field[] fields; // class fields, that is, its variables
071    private Method[] methods; // methods defined in the class
072    private Attribute[] attributes; // attributes defined in the class
073
074    private final boolean isZip; // Loaded from ZIP file
075
076    /**
077     * Parses class from the given stream.
078     *
079     * @param inputStream Input stream.
080     * @param fileName File name.
081     */
082    public ClassParser(final InputStream inputStream, final String fileName) {
083        this.fileName = fileName;
084        this.fileOwned = false;
085        final String clazz = inputStream.getClass().getName(); // Not a very clean solution ...
086        this.isZip = clazz.startsWith("java.util.zip.") || clazz.startsWith("java.util.jar.");
087        if (inputStream instanceof DataInputStream) {
088            this.dataInputStream = (DataInputStream) inputStream;
089        } else {
090            this.dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, BUFSIZE));
091        }
092    }
093
094    /**
095     * Parses class from given .class file.
096     *
097     * @param fileName file name.
098     */
099    public ClassParser(final String fileName) {
100        this.isZip = false;
101        this.fileName = fileName;
102        this.fileOwned = true;
103    }
104
105    /**
106     * Parses class from given .class file in a ZIP-archive
107     *
108     * @param zipFile ZIP file name.
109     * @param fileName file name.
110     */
111    public ClassParser(final String zipFile, final String fileName) {
112        this.isZip = true;
113        this.fileOwned = true;
114        this.zipFile = zipFile;
115        this.fileName = fileName;
116    }
117
118    /**
119     * Gets whether compact Code header parsing is enabled for class file versions 45.0 through 45.2.
120     *
121     * @return Whether compact Code header parsing is enabled.
122     * @since 6.14.0
123     * @see #setCompactCodeHeaderEnabled(boolean)
124     */
125    public boolean isCompactCodeHeaderEnabled() {
126        return compactCodeHeaderEnabled;
127    }
128
129    /**
130     * Parses the given Java class file and return an object that represents the contained data, that is, constants, methods,
131     * fields and commands. A <em>ClassFormatException</em> is raised, if the file is not a valid .class file. (This does
132     * not include verification of the byte code as it is performed by the Java interpreter).
133     *
134     * <p>
135     * Code attributes use the standard eight-byte header by default, including in class files with version 45.0 through 45.2.
136     * Use {@link #setCompactCodeHeaderEnabled(boolean)} to read the compact four-byte header used by historical files with those versions.
137     * </p>
138     *
139     * @return Class object representing the parsed class file.
140     * @throws IOException Thrown if an I/O error occurs.
141     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
142     */
143    public JavaClass parse() throws IOException, ClassFormatException {
144        ZipFile zip = null;
145        try {
146            if (fileOwned) {
147                if (isZip) {
148                    zip = new ZipFile(zipFile);
149                    final ZipEntry entry = zip.getEntry(fileName);
150
151                    if (entry == null) {
152                        throw new IOException("File " + fileName + " not found");
153                    }
154
155                    dataInputStream = new DataInputStream(new BufferedInputStream(zip.getInputStream(entry), BUFSIZE));
156                } else {
157                    dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(fileName), BUFSIZE));
158                }
159            }
160            // -- Read headers --
161            // Check magic tag of class file
162            readID();
163            // Get compiler version
164            readVersion();
165            // -- Read constant pool and related **************/
166            // Read constant pool entries
167            readConstantPool();
168            // Get class information
169            readClassInfo();
170            // Get interface information, that is, implemented interfaces
171            readInterfaces();
172            // -- Read class fields and methods --
173            // Read class fields, that is, the variables of the class
174            readFields();
175            // Read class methods, that is, the functions in the class
176            readMethods();
177            // Read class attributes
178            readAttributes();
179            // Check for unknown variables
180            // Unknown[] u = Unknown.getUnknownAttributes();
181            // for (int i=0; i < u.length; i++)
182            // System.err.println("WARNING: " + u[i]);
183            // Everything should have been read now
184            // if (file.available() > 0) {
185            // int bytes = file.available();
186            // byte[] buf = new byte[bytes];
187            // file.read(buf);
188            // if (!(isZip && (buf.length == 1))) {
189            // System.err.println("WARNING: Trailing garbage at end of " + fileName);
190            // System.err.println(bytes + " extra bytes: " + Utility.toHexString(buf));
191            // }
192            // }
193        } finally {
194            // Read everything of interest, so close the file
195            if (fileOwned) {
196                IOUtils.closeQuietly(dataInputStream);
197            }
198            IOUtils.closeQuietly(zip);
199        }
200        // Return the information we have gathered in a new object
201        return new JavaClass(classNameIndex, superclassNameIndex, fileName, major, minor, accessFlags, constantPool, interfaces, fields, methods, attributes,
202            isZip ? JavaClass.ZIP : JavaClass.FILE);
203    }
204
205    /**
206     * Reads information about the attributes of the class.
207     *
208     * @throws IOException Thrown if an I/O error occurs.
209     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
210     */
211    private void readAttributes() throws IOException, ClassFormatException {
212        final int attributesCount = dataInputStream.readUnsignedShort();
213        attributes = new Attribute[attributesCount];
214        for (int i = 0; i < attributesCount; i++) {
215            attributes[i] = Attribute.readAttribute(dataInputStream, constantPool, compactCodeHeader);
216        }
217    }
218
219    /**
220     * Reads information about the class and its superclass.
221     *
222     * @throws IOException Thrown if an I/O error occurs.
223     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
224     */
225    private void readClassInfo() throws IOException, ClassFormatException {
226        accessFlags = dataInputStream.readUnsignedShort();
227        /*
228         * Interfaces are implicitly abstract, the flag should be set according to the JVM specification.
229         */
230        if ((accessFlags & Const.ACC_INTERFACE) != 0) {
231            accessFlags |= Const.ACC_ABSTRACT;
232        }
233        if ((accessFlags & Const.ACC_ABSTRACT) != 0 && (accessFlags & Const.ACC_FINAL) != 0) {
234            throw new ClassFormatException("Class %s can't be both final and abstract", fileName);
235        }
236        classNameIndex = dataInputStream.readUnsignedShort();
237        superclassNameIndex = dataInputStream.readUnsignedShort();
238    }
239
240    /**
241     * Reads constant pool entries.
242     *
243     * @throws IOException Thrown if an I/O error occurs.
244     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
245     */
246    private void readConstantPool() throws IOException, ClassFormatException {
247        constantPool = new ConstantPool(dataInputStream);
248    }
249
250    /**
251     * Reads information about the fields of the class, that is, its variables.
252     *
253     * @throws IOException Thrown if an I/O error occurs.
254     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
255     */
256    private void readFields() throws IOException, ClassFormatException {
257        final int fieldsCount = dataInputStream.readUnsignedShort();
258        fields = new Field[fieldsCount];
259        for (int i = 0; i < fieldsCount; i++) {
260            fields[i] = new Field(dataInputStream, constantPool, compactCodeHeader);
261        }
262    }
263
264    /**
265     * Checks whether the header of the file is ok. Of course, this has to be the first action on successive file reads.
266     *
267     * @throws IOException Thrown if an I/O error occurs.
268     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
269     */
270    private void readID() throws IOException, ClassFormatException {
271        if (dataInputStream.readInt() != Const.JVM_CLASSFILE_MAGIC) {
272            throw new ClassFormatException("%s is not a Java .class file", fileName);
273        }
274    }
275
276    /**
277     * Reads information about the interfaces implemented by this class.
278     *
279     * @throws IOException Thrown if an I/O error occurs.
280     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
281     */
282    private void readInterfaces() throws IOException, ClassFormatException {
283        interfaces = readU2U2Table(dataInputStream);
284    }
285
286    /**
287     * Reads information about the methods of the class.
288     *
289     * @throws IOException Thrown if an I/O error occurs.
290     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
291     */
292    private void readMethods() throws IOException {
293        final int methodsCount = dataInputStream.readUnsignedShort();
294        methods = new Method[methodsCount];
295        for (int i = 0; i < methodsCount; i++) {
296            methods[i] = new Method(dataInputStream, constantPool, compactCodeHeader);
297        }
298    }
299
300    /**
301     * Reads major and minor version of compiler which created the file.
302     *
303     * @throws IOException Thrown if an I/O error occurs.
304     * @throws ClassFormatException Thrown if a class is malformed or cannot be interpreted as a class file.
305     */
306    private void readVersion() throws IOException, ClassFormatException {
307        minor = dataInputStream.readUnsignedShort();
308        major = dataInputStream.readUnsignedShort();
309        // The option resolves the ambiguous layout for 45.0 through 45.2; versions 45.3 and later always use standard headers.
310        compactCodeHeader = compactCodeHeaderEnabled && major == Const.MAJOR_1_1 && minor < Const.MINOR_1_1;
311    }
312
313    /**
314     * Sets whether to read compact Code headers in class file versions 45.0 through 45.2.
315     *
316     * <p>
317     * "Compact" refers to the widths of {@code max_stack}, {@code max_locals}, and {@code code_length}: unsigned one-, one-, and two-byte fields,
318     * respectively, totaling 4 bytes. Standard headers use unsigned two-, two-, and four-byte fields, totaling 8 bytes. These sizes exclude the common
319     * six-byte attribute prefix. The layout of that prefix and the remaining Code fields is unchanged.
320     * </p>
321     *
322     * <p>
323     * This option is disabled by default because modern JVMs also accept versions 45.0 through 45.2 with standard headers. The version therefore
324     * cannot determine the layout by itself; enabling this option selects the historical compact layout. Other class file versions always use standard
325     * headers, regardless of this option. Set this option before calling {@link #parse()}.
326     * </p>
327     *
328     * @param enabled Whether to enable compact Code header parsing.
329     * @since 6.14.0
330     * @see Code#isCompactCodeHeader()
331     */
332    public void setCompactCodeHeaderEnabled(final boolean enabled) {
333        compactCodeHeaderEnabled = enabled;
334    }
335}