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());
}
}
......
......@@ -211,6 +211,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.
*
......
This diff is collapsed.
......@@ -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) {
......@@ -243,7 +255,12 @@ public class ClassNode extends ClassVisitor {
for (InnerClassNode icn : innerClasses) {
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,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,22 +97,18 @@ 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.
* For each dex file contained in the test/case folder :
......@@ -110,48 +122,41 @@ 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();
// Executes Baksmali to disassemble the current dex file.
TestUtil.baksmali(new String[] { fullDexFileName,
"-o" + TestUtil.TEMP_FOLDER_EXPECTED});
// Uses the Tree API with the Reader and Writer to generate our own dex file from the current dex file.
ApplicationReader ar = new ApplicationReader(Opcodes.ASM4, fullDexFileName);
ApplicationNode an = new ApplicationNode(Opcodes.ASM4);
ar.accept(an, 0);
ApplicationWriter aw = new ApplicationWriter();
an.accept(aw);
byte[] generatedDexFile = aw.toByteArray();
String fullGeneratedDexFileName = TestUtil.TEMP_FOLDER_ROOT + TestUtil.FILENAME_GENERATED_DEX;
File createdDexFile = TestUtil.createFileFromByteArray(generatedDexFile, fullGeneratedDexFileName);
// Tests the maps of both the original dex file and the generated one.
assertTrue("Unequal Map between " + dexFileName + " and the generated file.", TestUtil.testMapDexFiles(createdDexFile, dexFile));
// Executes Baksmali once again to disassemble our generated dex file.
TestUtil.baksmali(new String[] { fullGeneratedDexFileName,
"-o" + TestUtil.TEMP_FOLDER_GENERATED});
// Compare the folders and the .smali files inside.
areFolderIdentical = TestUtil.testSmaliFoldersEquality(TestUtil.TEMP_FOLDER_GENERATED,
TestUtil.TEMP_FOLDER_EXPECTED, skipLineNumbers);
assertTrue("Generated .smali files differ.", areFolderIdentical);
TestUtil.removeTemporaryFolder();
}
}
return areFolderIdentical;
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});
// Uses the Tree API with the Reader and Writer to generate our own dex file from the current dex file.
ApplicationReader ar = new ApplicationReader(Opcodes.ASM4, fullDexFileName);
ApplicationNode an = new ApplicationNode(Opcodes.ASM4);
ar.accept(an, 0);
ApplicationWriter aw = new ApplicationWriter();
an.accept(aw);
byte[] generatedDexFile = aw.toByteArray();
String fullGeneratedDexFileName = TestUtil.TEMP_FOLDER_ROOT + TestUtil.FILENAME_GENERATED_DEX;
File createdDexFile = TestUtil.createFileFromByteArray(generatedDexFile, fullGeneratedDexFileName);
// Tests the maps of both the original dex file and the generated one.
assertTrue("Unequal Map between " + dexFileName + " and the generated file.", TestUtil.testMapDexFiles(createdDexFile, dexFile));
// Executes Baksmali once again to disassemble our generated dex file.
TestUtil.baksmali(new String[] { fullGeneratedDexFileName,
"-o" + TestUtil.TEMP_FOLDER_GENERATED});
// Compare the folders and the .smali files inside.
areFolderIdentical = TestUtil.testSmaliFoldersEquality(TestUtil.TEMP_FOLDER_GENERATED,
TestUtil.TEMP_FOLDER_EXPECTED, skipLineNumbers);
assertTrue("Generated .smali files differ.", areFolderIdentical);
TestUtil.removeTemporaryFolder();
}
}
......@@ -37,8 +37,13 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
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.Opcodes;
import org.ow2.asmdex.util.AsmDexifierApplicationVisitor;
......@@ -60,34 +65,35 @@ import org.ow2.asmdex.util.AsmDexifierApplicationVisitor;
*
* @author Julien Névo
*/
@RunWith(Parameterized.class)
public class AsmDexifierTest {
@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 AsmDexifierTest(File file) {
dexFile = file;
}
/**
* True to show little messages about what files is being tested.
*/
private static boolean SHOW_MESSAGES = true;
/**
* Test asm dexifier.
*
* @throws Exception the exception
*/
@Test
public void testAsmDexifier() throws Exception {
boolean result = true;
// Looks for all the dex files to test.
File testCaseFolder;
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.FULL_TEST_SUBFOLDER);
result = testAsmDexifierFolder(testCaseFolder, false);
testCaseFolder = new File(TestUtil.PATH_FOLDER_TESTCASE + TestUtil.SKIP_LINE_NUMBERS_TEST_SUBFOLDER);
result &= testAsmDexifierFolder(testCaseFolder, true);
assertTrue("Generated dex files aren't equal to the ones from dx.", result);
}
/**
* Tests if the generation to byte array is correct.
* For each dex file contained in the test/case folder :
......@@ -98,26 +104,22 @@ public class AsmDexifierTest {
* - Compares each .smali files :
* Allows some dex files to skip some debug information : the line numbers can be
* incorrect in some rare and not useful cases.
* @param skipLineNumbers true to skip the debug information about line numbers.
* @throws IOException
*/
private boolean testAsmDexifierFolder(File folder, boolean skipLineNumbers) throws Exception {
boolean result = true;
for (File dexFile : folder.listFiles()) {
String dexFileName = dexFile.getName();
if (dexFileName.toLowerCase().endsWith(".dex")) {
if (SHOW_MESSAGES) {
System.out.print("AsmDexifier test with " + dexFileName + ". ");
}
result = testDexFile(dexFile, skipLineNumbers);
assertTrue("Dex file isn't equal : " + dexFileName, result);
}
}
return result;
@Test
public void testAsmDexifier() throws Exception {
testDexFile(false);
}
/**
* Same test as testAsmDexifier without debug info
* @throws Exception
*/
@Test
public void testAsmDexifierSkip() throws Exception {
testDexFile(true);
}
/**
* Tests the given dex file.
* @param dexFile the dex file to test.
......@@ -126,7 +128,7 @@ public class AsmDexifierTest {
* @return true if the generated dex file matches the original.
* @throws Exception
*/
private static boolean testDexFile(File dexFile, boolean skipLineNumbers) throws Exception {
private boolean testDexFile(boolean skipLineNumbers) throws Exception {
TestUtil.removeTemporaryFolder();
TestUtil.createTemporaryFolders(new String[] { TestUtil.PATH_FOLDER_TEST, TestUtil.PATH_FOLDER_CONFORM });
......@@ -216,6 +218,8 @@ public class AsmDexifierTest {
TestUtil.removeTemporaryFolder();
assertTrue("Unequal smali folders between the original and the generated file.", areFolderIdentical);
if ((areFolderIdentical) && (SHOW_MESSAGES)) {
System.out.println("OK!");
}
......
......@@ -35,6 +35,7 @@ import org.objectweb.asmdex.logging.LogElementClassVisitAnnotation;
import org.objectweb.asmdex.logging.LogElementClassVisitEnd;
import org.objectweb.asmdex.logging.LogElementClassVisitField;
import org.objectweb.asmdex.logging.LogElementClassVisitInnerClass;
import org.objectweb.asmdex.logging.LogElementClassVisitMemberClass;
import org.objectweb.asmdex.logging.LogElementClassVisitMethod;
import org.objectweb.asmdex.logging.LogElementClassVisitOuterClass;
import org.objectweb.asmdex.logging.LogElementClassVisitSource;
......@@ -103,6 +104,10 @@ public class ClassTestVisitor extends ClassVisitor {
logger.foundElement(new LogElementClassVisitInnerClass(name, outerName, innerName, access));
}
@Override
public void visitMemberClass(String name, String outerName, String innerName) {
logger.foundElement(new LogElementClassVisitMemberClass(name, outerName, innerName));
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String[] signature, String[] exceptions) {
......
......@@ -341,10 +341,10 @@ public class LogListHelloWorld implements LogList {
, new LogElementApplicationVisitClass(ACC_PUBLIC + ACC_FINAL, "Lft/nevo/R;", null, "Ljava/lang/Object;", null)
, new LogElementClassVisit(ACC_PUBLIC + ACC_FINAL, "Lft/nevo/R;", null, "Ljava/lang/Object;", null)
, new LogElementClassVisitSource("R.java", null)
, new LogElementClassVisitInnerClass("Lft/nevo/R$attr;", "Lft/nevo/R;", "attr", ACC_PUBLIC + ACC_FINAL)
, new LogElementClassVisitInnerClass("Lft/nevo/R$drawable;", "Lft/nevo/R;", "drawable", ACC_PUBLIC + ACC_FINAL)
, new LogElementClassVisitInnerClass("Lft/nevo/R$layout;", "Lft/nevo/R;", "layout", ACC_PUBLIC + ACC_FINAL)
, new LogElementClassVisitInnerClass("Lft/nevo/R$string;", "Lft/nevo/R;", "string", ACC_PUBLIC + ACC_FINAL)
, new LogElementClassVisitMemberClass("Lft/nevo/R$attr;", "Lft/nevo/R;", "attr")
, new LogElementClassVisitMemberClass("Lft/nevo/R$drawable;", "Lft/nevo/R;", "drawable")
, new LogElementClassVisitMemberClass("Lft/nevo/R$layout;", "Lft/nevo/R;", "layout")
, new LogElementClassVisitMemberClass("Lft/nevo/R$string;", "Lft/nevo/R;", "string")
// Method <init>
, new LogElementClassVisitMethod(ACC_PUBLIC + ACC_CONSTRUCTOR, "<init>", "V", null, null)
......
......@@ -116,6 +116,10 @@ public abstract class LogElement {
* inner class visit
*/
TYPE_CLASS_VISIT_INNER_CLASS,
/**
* member class visit
*/
TYPE_CLASS_VISIT_MEMBER_CLASS,
/**
* method visit
*/
......
/* Software Name : AsmDex
* Version : 1.0
*
* Copyright © 2012 France Télécom
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holders nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF