Commit 4c656fe9 authored by pcregut's avatar pcregut

[Bug 316340]

- Changes to InnerClass annotation: creation of an explicit MemberClass annotation (no more test if name is null, this was wrong for named inner classes)
- InnerClass name computed from the real name of the class. It cannot be recomputed on named InnerClasses.
- Propagation of the MemberClass annotation to the structure of asmdex (tree visitor, logelements, etc.)
- Modification of the test case structure to get more visibility on what is wrong (parameterized test suite)
- Updated Junit.
- Added test case for Inner and Member class covering all the known variants (static or not, named or not, member not inner).
parent 6d2068cf
......@@ -4,6 +4,6 @@
<classpathentry kind="src" path="test/conform"/>
<classpathentry kind="src" path="test/perf"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="lib" path="lib/junit-4.8.2.jar"/>
<classpathentry kind="lib" path="lib/junit-4.10.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
......@@ -577,7 +577,7 @@ public class ApplicationReader {
// Visits the inner class, if any.
if (classAnnotationsOffset != 0) {
dexFile.seek(classAnnotationsOffset); // Get to the annotation_set_item.
readInnerClassAnnotations(classVisitor);
readInnerClassAnnotations(className, classVisitor);
dexFile.seek(classAnnotationsOffset);
readMemberClassesAnnotations(classVisitor);
......@@ -1342,7 +1342,8 @@ public class ApplicationReader {
// Obfuscators may remove mention of the inner name. We must cope with it.
String outerName =
(i<0) ? null : name.substring(0, i) + ";"; // Adds the ";" at the end.
classVisitor.visitInnerClass(name, outerName, innerName, accessFlags);
// classVisitor.visitInnerClass(name, outerName, innerName, accessFlags);
classVisitor.visitMemberClass(name, outerName, innerName);
}
}
}
......@@ -1351,9 +1352,10 @@ public class ApplicationReader {
* Reads the Annotations, check only the ones related to inner classes
* (EnclosingClass, InnerClasses), and call the visitInnerClass accordingly.
* The dex file reader must point on an annotation_set_item.
* @param className
* @param classVisitor the visitor to visit the inner class.
*/
private void readInnerClassAnnotations(ClassVisitor classVisitor) {
private void readInnerClassAnnotations(String className, ClassVisitor classVisitor) {
// Creates the InnerClass and EnclosingClass Parsers and makes the search for these
// specific annotations in the annotations.
......@@ -1373,17 +1375,9 @@ public class ApplicationReader {
EnclosingClassSpecificAnnotationParser enclosingParser = (EnclosingClassSpecificAnnotationParser)enclosingClassSpecificAnnotationParser;
// Gets the information.
//int classId = enclosingParser.getClassId();
//String outerClassName = dexFile.getStringItemFromTypeIndex(classId);
String outerClassName = enclosingParser.getClassName();
String simpleNameInnerClass = innerParser.getSimpleNameInnerClass();
String nameInnerClass = null;
// Reconstruction of the name of the inner class.
if ((simpleNameInnerClass != null) && (outerClassName != null)) {
nameInnerClass = outerClassName.replace(';', '$') + simpleNameInnerClass + ';';
}
classVisitor.visitInnerClass(nameInnerClass, outerClassName, simpleNameInnerClass, innerParser.getAccessFlagsInnerClass());
classVisitor.visitInnerClass(className, outerClassName, simpleNameInnerClass, innerParser.getAccessFlagsInnerClass());
}
}
......@@ -1431,10 +1425,16 @@ public class ApplicationReader {
String outerClassName = dexFile.getNameFromMethodIndex(methodId);
String nameInnerClass = null;
// Reconstruction the name of the inner class.
/*
if ((nameEnclosingClass != null) && (simpleNameInnerClass != null) && (outerClassName != null)) {
nameInnerClass = nameEnclosingClass.replace(';', '$') + '1' + simpleNameInnerClass;
}
classVisitor.visitInnerClass(nameInnerClass, outerClassName, simpleNameInnerClass, innerParser.getAccessFlagsInnerClass());
*/
if ((nameEnclosingClass != null) && (simpleNameInnerClass != null) && (outerClassName != null)) {
nameInnerClass = nameEnclosingClass.replace(';', '$') + '1' + simpleNameInnerClass + ";";
}
classVisitor.visitInnerClass(nameInnerClass, nameEnclosingClass, simpleNameInnerClass, innerParser.getAccessFlagsInnerClass());
}
}
......
......@@ -212,6 +212,22 @@ public abstract class ClassVisitor {
}
/**
* Visits information about a member class.
*
* @param outerName the internal name of the class to which the member class
* belongs.
* @param innerName the (simple) name of the member class inside its
* enclosing class.
*/
public void visitMemberClass(
String name,
String outerName,
String innerName) {
if (cv != null) {
cv.visitMemberClass(name,outerName, innerName);
}
}
/**
* Visits a field of the class.
*
* @param access the field's access flags (see {@link Opcodes}). This
......
......@@ -410,11 +410,6 @@ public class ClassWriter extends ClassVisitor {
constantPool.addStringToConstantPool(innerName);
}
// According to the given parameters, we can know if we're visiting an Inner Class, or if
// we're declaring them. If the name of the InnerClass is the same as the current Class, we're
// visiting it.
// Also, if name is Null means an anonymous class.
if ((name == null) || (name.equals(classDefinitionItem.getClassName()))) {
// We're inside an Inner Class, visiting it.
// Two annotations need to be created. The Enclosing Class/Method, and the Inner Class.
// If the enclosingMethod reference is null, we create an Enclosing Class, else an Enclosing
......@@ -468,18 +463,15 @@ public class ClassWriter extends ClassVisitor {
constantPool.addAnnotationItemToConstantPool(annotationItem);
classDefinitionItem.addAnnotationItem(annotationItem);
} else {
// We're declaring an Inner Class.
// One annotation needs to be created, the MemberClasses Annotation.
// However, we don't create it now because it needs may be filled by several calls to
// the visitInnerClass() method. On the first call, we create an Encoded Array which we store.
// On each call, we add the newly reference to an Inner Class inside this Array.
// When the class visit has ended, we test if the Array exists. If yes, we build an annotation around
// it, and adds to the Class and the Constant Pool.
classDefinitionItem.addMemberClassValue(name, constantPool);
}
@Override
public void visitMemberClass(String name, String outerName,
String innerName) {
if (name != null) {
constantPool.addTypeToConstantPool(name);
}
classDefinitionItem.addMemberClassValue(name, constantPool);
}
@Override
......
......@@ -105,6 +105,11 @@ public class ClassNode extends ClassVisitor {
*/
public List<InnerClassNode> innerClasses = new ArrayList<InnerClassNode>();
/**
* Informations about the member classes of this class.
*/
public List<MemberClassNode> memberClasses = new ArrayList<MemberClassNode>();
/**
* The fields of this class.
*/
......@@ -180,6 +185,13 @@ public class ClassNode extends ClassVisitor {
innerClasses.add(n);
}
@Override
public void visitMemberClass(String name, String outerName,
String innerName) {
MemberClassNode n = new MemberClassNode(name, outerName, innerName);
memberClasses.add(n);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String[] signature, String[] exceptions) {
......@@ -244,6 +256,11 @@ public class ClassNode extends ClassVisitor {
icn.accept(cv);
}
// Visits Inner Classes.
for (MemberClassNode icn : memberClasses) {
icn.accept(cv);
}
// Visits Fields.
for (FieldNode fn : fields) {
fn.accept(cv);
......
package org.ow2.asmdex.tree;
import org.ow2.asmdex.ClassVisitor;
/**
* A node that represents a member class (specific to ASMDEX)
*
* @author Pierre Cregut
*/
public class MemberClassNode {
/**
* The internal name of an inner class.
*/
public String name;
/**
* The internal name of the class to which the inner class belongs. May
* be <tt>null</tt>.
*/
public String outerName;
/**
* The (simple) name of the inner class inside its enclosing class. May be
* <tt>null</tt> for anonymous inner classes.
*/
public String innerName;
/**
* Constructs a new {@link InnerClassNode}.
*
* @param name the internal name of an inner class.
* @param outerName the internal name of the class to which the inner class
* belongs. May be <tt>null</tt>.
* @param innerName the (simple) name of the inner class inside its
* enclosing class. May be <tt>null</tt> for anonymous inner
* classes.
*/
public MemberClassNode(
final String name,
final String outerName,
final String innerName)
{
this.name = name;
this.outerName = outerName;
this.innerName = innerName;
}
/**
* Makes the given class visitor visit this inner class.
*
* @param cv a class visitor.
*/
public void accept(final ClassVisitor cv) {
cv.visitMemberClass(name, outerName, innerName);
}
}
......@@ -150,6 +150,19 @@ public class AsmDexifierClassVisitor extends ClassVisitor {
pr.closeText();
}
@Override
public void visitMemberClass(String name, String outerName, String innerName) {
pr.addTabulation();
pr.addText("cv.visitMemberClass(");
pr.addConstant(name, true);
pr.addConstant(outerName, true);
pr.addConstant(innerName, false);
pr.addText(");");
pr.addEOL();
pr.closeText();
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String[] signature, String[] exceptions) {
pr.addTabulation();
......
package org.objectweb.asmdex;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.ow2.asmdex.ApplicationReader;
import org.ow2.asmdex.ApplicationWriter;
import org.ow2.asmdex.Opcodes;
import java.io.IOException;
public class ApplicationWriterBasicTest {
/**
* Tests the visit method, but it does nothing.
*/
@Test
public void testVisit() {
ApplicationWriter aw = new ApplicationWriter();
aw.visit();
}
/**
* Tests the visitClass method, but it does nothing.
*/
@Test
public void testVisitClass() {
ApplicationWriter aw = new ApplicationWriter();
aw.visitClass(0, "class", null, null, null);
}
/**
* Tests that a newly created Writer doesn't provide a byte array.
*/
@Test
public void testToByteArrayNull() {
ApplicationWriter aw = new ApplicationWriter();
assertNull(aw.toByteArray());
}
/**
* Tests that a newly created Writer provides a byte array.
*/
@Test
public void testGetConstantPool() {
ApplicationWriter aw = new ApplicationWriter();
assertNotNull(aw.getConstantPool());
}
/**
* Tests the visitEnd method, which generated the dex file, it should only contain the header and
* the Map at the end.
*/
@Test
public void testVisitEndAlmostEmpty() {
ApplicationWriter aw = new ApplicationWriter();
aw.visitEnd();
byte[] bytes = aw.toByteArray();
assertEquals(0x70 + 0x1c, bytes.length);
}
/**
* Tests that a newly created Writer doesn't provide an Application Reader unless it is given to it.
*/
@Test
public void testGetApplicationReaderNull() {
ApplicationWriter aw = new ApplicationWriter();
assertNull(aw.getApplicationReader());
}
/**
* Tests that a newly created Writer provides an Application Reader if it is given to it.
* @throws IOException
*/
@Test
public void testGetApplicationReader() throws IOException {
ApplicationReader ar = new ApplicationReader(Opcodes.ASM4, TestUtil.PATH_AND_FILENAME_HELLO_WORLD_DEX);
ApplicationWriter aw = new ApplicationWriter(ar);
assertNotNull(aw.getApplicationReader());
}
}
......@@ -30,17 +30,22 @@
*/
package org.objectweb.asmdex;
import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.assertTrue;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.ow2.asmdex.ApplicationReader;
import org.ow2.asmdex.ApplicationWriter;
import org.ow2.asmdex.Opcodes;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
/**
* Tests the Application Writer.
* This is done by comparing the output of the Writer, fed by the Reader, and the output of Baksmali.
......@@ -52,92 +57,72 @@ import org.ow2.asmdex.Opcodes;
*
* @author Julien Névo
*/
@RunWith(Parameterized.class)
public class ApplicationWriterTest {
/**
* After the class has been tested, we remove the temporary folder.
*/
@AfterClass
public static void testAfter() {
TestUtil.removeTemporaryFolder();
}
/**
* Test application writer.
* Parameters for the test : set of files.
* @return collection of dex files
*/
@Test
public void testApplicationWriter() {
// This constructor does nothing.
new ApplicationWriter();
@Parameters
public static Collection<Object[]> data() {
ArrayList<Object[]> data = new ArrayList<Object[]>();
File testCaseFolder;
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.FULL_TEST_SUBFOLDER);
for (File dexFile : testCaseFolder.listFiles()) {
String dexFileName = dexFile.getName();
if (dexFileName.toLowerCase().endsWith(".dex")) {
data.add(new Object [] {dexFile});
}
/**
* Tests the visit method, but it does nothing.
*/
@Test
public void testVisit() {
ApplicationWriter aw = new ApplicationWriter();
aw.visit();
}
/**
* Tests the visitClass method, but it does nothing.
*/
@Test
public void testVisitClass() {
ApplicationWriter aw = new ApplicationWriter();
aw.visitClass(0, "class", null, null, null);
return data;
}
/**
* Tests that a newly created Writer doesn't provide a byte array.
*/
@Test
public void testToByteArrayNull() {
ApplicationWriter aw = new ApplicationWriter();
assertNull(aw.toByteArray());
}
private File dexFile;
/**
* Tests that a newly created Writer provides a byte array.
* Parameterized constructor taking the name of the file (classes.dex)
* @param file
*/
@Test
public void testGetConstantPool() {
ApplicationWriter aw = new ApplicationWriter();
assertNotNull(aw.getConstantPool());
public ApplicationWriterTest(File file) {
dexFile = file;
}
/**
* Tests the visitEnd method, which generated the dex file, it should only contain the header and
* the Map at the end.
* After the class has been tested, we remove the temporary folder.
*/
@Test
public void testVisitEndAlmostEmpty() {
ApplicationWriter aw = new ApplicationWriter();
aw.visitEnd();
byte[] bytes = aw.toByteArray();
assertEquals(0x70 + 0x1c, bytes.length);
@AfterClass
public static void testAfter() {
TestUtil.removeTemporaryFolder();
}
/**
* Tests that a newly created Writer doesn't provide an Application Reader unless it is given to it.
* Tests if the generation to byte array is correct.
* For each dex file contained in the test/case folder :
* - Runs Baksmali to create all the .smali file in the baksmali temporary folder.
* - Runs Baksmali to create the files generated by the writer, in a second temporary folder.
* - Compares the Maps information of both dex files. They should have the same
* number of elements.
* - Compares each .smali files :
* Allows some dex files to skip some debug information : line number can be
* incorrect in some rare and not useful cases.
*/
@Test
public void testGetApplicationReaderNull() {
ApplicationWriter aw = new ApplicationWriter();
assertNull(aw.getApplicationReader());
public void testToByteArray() throws IOException {
testGenerationToByteArray(false,false);
}
/**
* Tests that a newly created Writer provides an Application Reader if it is given to it.
* Tests if the generation to byte array is correct, with the shortcut optimization.
* @throws IOException
*/
@Test
public void testGetApplicationReader() throws IOException {
ApplicationReader ar = new ApplicationReader(Opcodes.ASM4, TestUtil.PATH_AND_FILENAME_HELLO_WORLD_DEX);
ApplicationWriter aw = new ApplicationWriter(ar);
assertNotNull(aw.getApplicationReader());
public void testToByteArrayOptimization() throws IOException {
testGenerationToByteArray(false,true);
}
/**
......@@ -152,9 +137,8 @@ public class ApplicationWriterTest {
* incorrect in some rare and not useful cases.
*/
@Test
public void testToByteArray() throws IOException {
boolean result = testToByteArray(false);
assertTrue("Generated dex files aren't equal to the ones from dx.", result);
public void testToByteArraySkip() throws IOException {
testGenerationToByteArray(true,false);
}
/**
......@@ -162,33 +146,10 @@ public class ApplicationWriterTest {
* @throws IOException
*/
@Test
public void testToByteArrayOptimization() throws IOException {
boolean result = testToByteArray(true);
assertTrue("Generated dex files aren't equal to the ones from dx.", result);
public void testToByteArrayOptimizationSkip() throws IOException {
testGenerationToByteArray(true,true);
}
/**
* Tests effectively the generation to byte array.
* @param shortCutOptimization true to use the shortcut optimization.
* @return true if the test performed fine.
* @throws IOException
*/
private boolean testToByteArray(boolean shortCutOptimization) throws IOException {
boolean result;
// Looks for all the dex files to test.
File testCaseFolder;
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.FULL_TEST_SUBFOLDER);
result = testGenerationToByteArray(testCaseFolder, false, shortCutOptimization);
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.SKIP_LINE_NUMBERS_TEST_SUBFOLDER);
result &= testGenerationToByteArray(testCaseFolder, true, shortCutOptimization);
return result;
}
/**
* Tests if the generation to byte array is correct.
......@@ -204,13 +165,9 @@ public class ApplicationWriterTest {
* @param shortCutOptimization true to use the shortcut optimization.
* @throws IOException
*/
private boolean testGenerationToByteArray(File folder, boolean skipLineNumbers,
boolean shortCutOptimization) throws IOException {
boolean areFolderIdentical = true;
for (File dexFile : folder.listFiles()) {
private void testGenerationToByteArray(boolean skipLineNumbers,boolean shortCutOptimization) throws IOException {
String dexFileName = dexFile.getName();
if (dexFileName.toLowerCase().endsWith(".dex")) {
File folder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.FULL_TEST_SUBFOLDER);
String fullDexFileName = folder.getPath() + "/" + dexFileName;
TestUtil.removeTemporaryFolder();
......@@ -243,14 +200,10 @@ public class ApplicationWriterTest {
"-o" + TestUtil.TEMP_FOLDER_GENERATED});
// Compare the folders and the .smali files inside.
areFolderIdentical = TestUtil.testSmaliFoldersEquality(TestUtil.TEMP_FOLDER_GENERATED,
boolean areFolderIdentical = TestUtil.testSmaliFoldersEquality(TestUtil.TEMP_FOLDER_GENERATED,
TestUtil.TEMP_FOLDER_EXPECTED, skipLineNumbers);
assertTrue("Generated .smali files differ.", areFolderIdentical);
TestUtil.removeTemporaryFolder();
}
}
return areFolderIdentical;
}
}
......@@ -30,26 +30,52 @@
*/
package org.objectweb.asmdex;
import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.assertTrue;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.ow2.asmdex.ApplicationReader;
import org.ow2.asmdex.ApplicationWriter;
import org.ow2.asmdex.Opcodes;
import org.ow2.asmdex.tree.ApplicationNode;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
/**
* Tests the Application Writer using the Tree API.
* This is done by comparing the output of the Writer, fed by the Reader, and the output of Baksmali.
*
* @author Julien Névo
*/
@RunWith(Parameterized.class)
public class ApplicationWriterTreeTest {
@Parameters
public static Collection<Object[]> data() {
ArrayList<Object[]> data = new ArrayList<Object[]>();
File testCaseFolder;
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.FULL_TEST_SUBFOLDER);
for (File dexFile : testCaseFolder.listFiles()) {
String dexFileName = dexFile.getName();
if (dexFileName.toLowerCase().endsWith(".dex")) {
data.add(new Object [] {dexFile});
}
}
return data;
}
private File dexFile;
public ApplicationWriterTreeTest(File file) {
dexFile = file;
}
/**
* After the class has been tested, we remove the temporary folder.
*/
......@@ -58,16 +84,6 @@ public class ApplicationWriterTreeTest {
TestUtil.removeTemporaryFolder();
}
/**
* Test application writer.
*/
@Test
public void testApplicationWriter() {
// This constructor does nothing.
new ApplicationWriter();
}
/**
* Tests if the generation to byte array is correct.
* For each dex file contained in the test/case folder :
......@@ -81,21 +97,17 @@ public class ApplicationWriterTreeTest {
*/
@Test
public void testToByteArray() throws IOException {
boolean result;
// Looks for all the dex files to test.
File testCaseFolder;
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.FULL_TEST_SUBFOLDER);
result = testGenerationToByteArray(testCaseFolder, false);
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.SKIP_LINE_NUMBERS_TEST_SUBFOLDER);
result &= testGenerationToByteArray(testCaseFolder, true);
assertTrue("Generated dex files aren't equal to the ones from dx.", result);
testGenerationToByteArray(false);
}
@Test
public void testToByteArraySkipLine() throws IOException {
testGenerationToByteArray(true);
}
public String getName() {
return "testByteArray" + dexFile.getName();
}
/**
* Tests if the generation to byte array is correct.
......@@ -110,16 +122,12 @@ public class ApplicationWriterTreeTest {
* @param skipLineNumbers true to skip the debug information about line numbers.
* @throws IOException
*/
private boolean testGenerationToByteArray(File folder, boolean skipLineNumbers) throws IOException {
private void testGenerationToByteArray(boolean skipLineNumbers) throws IOException {
boolean areFolderIdentical = true;
for (File dexFile : folder.listFiles()) {
String dexFileName = dexFile.getName();
if (dexFileName.toLowerCase().endsWith(".dex")) {
String fullDexFileName = folder.getPath() + "/" + dexFileName;
TestUtil.removeTemporaryFolder();
File testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.FULL_TEST_SUBFOLDER);
String dexFileName = dexFile.getName();
String fullDexFileName = testCaseFolder.getPath() + "/" + dexFileName;
// Executes Baksmali to disassemble the current dex file.
TestUtil.baksmali(new String[] { fullDexFileName,
"-o" + TestUtil.TEMP_FOLDER_EXPECTED});
......@@ -150,8 +158,5 @@ public class ApplicationWriterTreeTest {
assertTrue("Generated .smali files differ.", areFolderIdentical);
TestUtil.removeTemporaryFolder();