Unverified Commit ed385e8c authored by skylot's avatar skylot Committed by GitHub

feat: output decompilation results in json format (#676)

parent 554e119e
......@@ -66,6 +66,8 @@ options:
-j, --threads-count - processing threads count
-r, --no-res - do not decode resources
-s, --no-src - do not decompile source code
--single-class - decompile a single class
--output-format - can be 'java' or 'json' (default: java)
-e, --export-gradle - save as android gradle project
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-imports - disable use of imports, always write entire package name
......
......@@ -112,6 +112,16 @@ public class JCommanderWrapper<T> {
// ignore
}
}
if (fieldType == String.class) {
try {
String val = (String) f.get(args);
if (val != null) {
opt.append(" (default: ").append(val).append(')');
}
} catch (Exception e) {
// ignore
}
}
}
private static void addSpaces(StringBuilder str, int count) {
......
......@@ -46,6 +46,9 @@ public class JadxCLIArgs {
@Parameter(names = { "--single-class" }, description = "decompile a single class")
protected String singleClass = null;
@Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'")
protected String outputFormat = "java";
@Parameter(names = { "-e", "--export-gradle" }, description = "save as android gradle project")
protected boolean exportAsGradleProject = false;
......@@ -86,7 +89,18 @@ public class JadxCLIArgs {
protected boolean deobfuscationForceSave = false;
@Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias")
protected boolean deobfuscationUseSourceNameAsAlias = true;
protected boolean deobfuscationUseSourceNameAsAlias = false;
@Parameter(
names = { "--rename-flags" },
description = "what to rename, comma-separated,"
+ " 'case' for system case sensitivity,"
+ " 'valid' for java identifiers,"
+ " 'printable' characters,"
+ " 'none' or 'all' (default)",
converter = RenameConverter.class
)
protected Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class);
@Parameter(names = { "--fs-case-sensitive" }, description = "treat filesystem as case sensitive, false by default")
protected boolean fsCaseSensitive = false;
......@@ -100,17 +114,6 @@ public class JadxCLIArgs {
@Parameter(names = { "-f", "--fallback" }, description = "make simple dump (using goto instead of 'if', 'for', etc)")
protected boolean fallbackMode = false;
@Parameter(
names = { "--rename-flags" },
description = "what to rename, comma-separated,"
+ " 'case' for system case sensitivity,"
+ " 'valid' for java identifiers,"
+ " 'printable' characters,"
+ " 'none' or 'all' (default)",
converter = RenameConverter.class
)
protected Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class);
@Parameter(names = { "-v", "--verbose" }, description = "verbose output")
protected boolean verbose = false;
......@@ -178,6 +181,7 @@ public class JadxCLIArgs {
args.setOutDir(FileUtils.toFile(outDir));
args.setOutDirSrc(FileUtils.toFile(outDirSrc));
args.setOutDirRes(FileUtils.toFile(outDirRes));
args.setOutputFormat(JadxArgs.OutputFormatEnum.valueOf(outputFormat.toUpperCase()));
args.setThreadsCount(threadsCount);
args.setSkipSources(skipSources);
if (singleClass != null) {
......
......@@ -8,6 +8,7 @@ dependencies {
compile 'org.ow2.asm:asm:7.1'
compile 'org.jetbrains:annotations:17.0.0'
compile 'uk.com.robust-it:cloning:1.9.12'
compile 'com.google.code.gson:gson:2.8.5'
compile 'org.smali:baksmali:2.2.7'
compile('org.smali:smali:2.2.7') {
......
......@@ -57,6 +57,14 @@ public final class CodePosition {
@Override
public String toString() {
return line + ':' + offset + (node != null ? " " + node : "");
StringBuilder sb = new StringBuilder();
sb.append(line);
if (offset != 0) {
sb.append(':').append(offset);
}
if (node != null) {
sb.append(' ').append(node);
}
return sb.toString();
}
}
......@@ -62,6 +62,12 @@ public class JadxArgs {
private Set<RenameEnum> renameFlags = EnumSet.allOf(RenameEnum.class);
public enum OutputFormatEnum {
JAVA, JSON
}
private OutputFormatEnum outputFormat = OutputFormatEnum.JAVA;
public JadxArgs() {
// use default options
}
......@@ -308,6 +314,18 @@ public class JadxArgs {
}
}
public OutputFormatEnum getOutputFormat() {
return outputFormat;
}
public boolean isJsonOutput() {
return outputFormat == OutputFormatEnum.JSON;
}
public void setOutputFormat(OutputFormatEnum outputFormat) {
this.outputFormat = outputFormat;
}
@Override
public String toString() {
return "JadxArgs{" + "inputFiles=" + inputFiles
......@@ -333,6 +351,7 @@ public class JadxArgs {
+ ", exportAsGradleProject=" + exportAsGradleProject
+ ", fsCaseSensitive=" + fsCaseSensitive
+ ", renameFlags=" + renameFlags
+ ", outputFormat=" + outputFormat
+ '}';
}
}
......@@ -215,7 +215,7 @@ public final class JadxDecompiler {
executor.execute(() -> {
try {
cls.decompile();
SaveCode.save(outDir, args, cls.getClassNode());
SaveCode.save(outDir, cls.getClassNode());
} catch (Exception e) {
LOG.error("Error saving class: {}", cls.getFullName(), e);
}
......
......@@ -19,7 +19,6 @@ import jadx.core.dex.attributes.nodes.EnumClassAttr;
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.attributes.nodes.LineAttrNode;
import jadx.core.dex.attributes.nodes.SourceFileAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.args.ArgType;
......@@ -128,8 +127,8 @@ public class ClassGen {
}
annotationGen.addForClass(clsCode);
insertSourceFileInfo(clsCode, cls);
insertRenameInfo(clsCode, cls);
CodeGenUtils.addSourceFileInfo(clsCode, cls);
clsCode.startLine(af.makeString());
if (af.isInterface()) {
if (af.isAnnotation()) {
......@@ -290,29 +289,21 @@ public class ClassGen {
return false;
}
private void addMethod(CodeWriter code, MethodNode mth) throws CodegenException {
public void addMethod(CodeWriter code, MethodNode mth) throws CodegenException {
CodeGenUtils.addComments(code, mth);
if (mth.getAccessFlags().isAbstract() || mth.getAccessFlags().isNative()) {
MethodGen mthGen = new MethodGen(this, mth);
mthGen.addDefinition(code);
if (cls.getAccessFlags().isAnnotation()) {
Object def = annotationGen.getAnnotationDefaultValue(mth.getName());
if (def != null) {
code.add(" default ");
annotationGen.encodeValue(code, def);
}
}
code.add(';');
} else {
CodeGenUtils.addComments(code, mth);
insertDecompilationProblems(code, mth);
boolean badCode = mth.contains(AFlag.INCONSISTENT_CODE);
if (badCode && showInconsistentCode) {
code.startLine("/* Code decompiled incorrectly, please refer to instructions dump. */");
mth.remove(AFlag.INCONSISTENT_CODE);
badCode = false;
}
MethodGen mthGen;
if (badCode || mth.contains(AType.JADX_ERROR) || fallback) {
if (badCode || fallback || mth.contains(AType.JADX_ERROR) || mth.getRegion() == null) {
mthGen = MethodGen.getFallbackMethodGen(mth);
} else {
mthGen = new MethodGen(this, mth);
......@@ -322,12 +313,7 @@ public class ClassGen {
}
code.add('{');
code.incIndent();
insertSourceFileInfo(code, mth);
if (fallback) {
mthGen.addFallbackMethodCode(code);
} else {
mthGen.addInstructions(code);
}
mthGen.addInstructions(code);
code.decIndent();
code.startLine('}');
}
......@@ -357,37 +343,41 @@ public class ClassGen {
private void addFields(CodeWriter code) throws CodegenException {
addEnumFields(code);
for (FieldNode f : cls.getFields()) {
if (f.contains(AFlag.DONT_GENERATE)) {
continue;
}
CodeGenUtils.addComments(code, f);
annotationGen.addForField(code, f);
addField(code, f);
}
}
if (f.getFieldInfo().isRenamed()) {
code.newLine();
CodeGenUtils.addRenamedComment(code, f, f.getName());
}
code.startLine(f.getAccessFlags().makeString());
useType(code, f.getType());
code.add(' ');
code.attachDefinition(f);
code.add(f.getAlias());
FieldInitAttr fv = f.get(AType.FIELD_INIT);
if (fv != null) {
code.add(" = ");
if (fv.getValue() == null) {
code.add(TypeGen.literalToString(0, f.getType(), cls, fallback));
} else {
if (fv.getValueType() == InitType.CONST) {
annotationGen.encodeValue(code, fv.getValue());
} else if (fv.getValueType() == InitType.INSN) {
InsnGen insnGen = makeInsnGen(fv.getInsnMth());
addInsnBody(insnGen, code, fv.getInsn());
}
public void addField(CodeWriter code, FieldNode f) {
if (f.contains(AFlag.DONT_GENERATE)) {
return;
}
CodeGenUtils.addComments(code, f);
annotationGen.addForField(code, f);
if (f.getFieldInfo().isRenamed()) {
code.newLine();
CodeGenUtils.addRenamedComment(code, f, f.getName());
}
code.startLine(f.getAccessFlags().makeString());
useType(code, f.getType());
code.add(' ');
code.attachDefinition(f);
code.add(f.getAlias());
FieldInitAttr fv = f.get(AType.FIELD_INIT);
if (fv != null) {
code.add(" = ");
if (fv.getValue() == null) {
code.add(TypeGen.literalToString(0, f.getType(), cls, fallback));
} else {
if (fv.getValueType() == InitType.CONST) {
annotationGen.encodeValue(code, fv.getValue());
} else if (fv.getValueType() == InitType.INSN) {
InsnGen insnGen = makeInsnGen(fv.getInsnMth());
addInsnBody(insnGen, code, fv.getInsn());
}
}
code.add(';');
}
code.add(';');
}
private boolean isFieldsPresents() {
......@@ -569,7 +559,7 @@ public class ClassGen {
}
}
private Set<ClassInfo> getImports() {
public Set<ClassInfo> getImports() {
if (parentGen != null) {
return parentGen.getImports();
} else {
......@@ -615,13 +605,6 @@ public class ClassGen {
return searchCollision(dex, useCls.getParentClass(), searchCls);
}
private void insertSourceFileInfo(CodeWriter code, AttrNode node) {
SourceFileAttr sourceFileAttr = node.get(AType.SOURCE_FILE);
if (sourceFileAttr != null) {
code.startLine("/* compiled from: ").add(sourceFileAttr.getFileName()).add(" */");
}
}
private void insertRenameInfo(CodeWriter code, ClassNode cls) {
ClassInfo classInfo = cls.getClassInfo();
if (classInfo.hasAlias()) {
......
package jadx.core.codegen;
import java.util.concurrent.Callable;
import jadx.api.JadxArgs;
import jadx.core.codegen.json.JsonCodeGen;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.utils.exceptions.CodegenException;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class CodeGen {
public static void generate(ClassNode cls) throws CodegenException {
public static void generate(ClassNode cls) {
if (cls.contains(AFlag.DONT_GENERATE)) {
cls.setCode(CodeWriter.EMPTY);
} else {
ClassGen clsGen = new ClassGen(cls, cls.root().getArgs());
CodeWriter code;
try {
code = clsGen.makeClass();
} catch (Exception e) {
if (cls.contains(AFlag.RESTART_CODEGEN)) {
cls.remove(AFlag.RESTART_CODEGEN);
code = clsGen.makeClass();
} else {
throw new JadxRuntimeException("Code generation error", e);
JadxArgs args = cls.root().getArgs();
switch (args.getOutputFormat()) {
case JAVA:
generateJavaCode(cls, args);
break;
case JSON:
generateJson(cls);
break;
}
}
}
private static void generateJavaCode(ClassNode cls, JadxArgs args) {
ClassGen clsGen = new ClassGen(cls, args);
CodeWriter code = wrapCodeGen(cls, clsGen::makeClass);
cls.setCode(code);
}
private static void generateJson(ClassNode cls) {
JsonCodeGen codeGen = new JsonCodeGen(cls);
String clsJson = wrapCodeGen(cls, codeGen::process);
cls.setCode(new CodeWriter(clsJson));
}
private static <R> R wrapCodeGen(ClassNode cls, Callable<R> codeGenFunc) {
try {
return codeGenFunc.call();
} catch (Exception e) {
if (cls.contains(AFlag.RESTART_CODEGEN)) {
cls.remove(AFlag.RESTART_CODEGEN);
try {
return codeGenFunc.call();
} catch (Exception ex) {
throw new JadxRuntimeException("Code generation error after restart", ex);
}
} else {
throw new JadxRuntimeException("Code generation error", e);
}
cls.setCode(code);
}
}
......
......@@ -4,7 +4,6 @@ import java.io.File;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
......@@ -37,7 +36,7 @@ public class CodeWriter {
INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR,
};
private StringBuilder buf = new StringBuilder();
private StringBuilder buf;
@Nullable
private String code;
private String indentStr;
......@@ -49,6 +48,7 @@ public class CodeWriter {
private Map<Integer, Integer> lineMap = Collections.emptyMap();
public CodeWriter() {
this.buf = new StringBuilder();
this.indent = 0;
this.indentStr = "";
if (ADD_LINE_NUMBERS) {
......@@ -56,6 +56,12 @@ public class CodeWriter {
}
}
// create filled instance (just string wrapper)
public CodeWriter(String code) {
this.buf = null;
this.code = code;
}
public CodeWriter startLine() {
addLine();
addLineIndent();
......@@ -225,6 +231,10 @@ public class CodeWriter {
attachAnnotation(obj, new CodePosition(line, offset + 1));
}
public void attachLineAnnotation(Object obj) {
attachAnnotation(obj, new CodePosition(line, 0));
}
private Object attachAnnotation(Object obj, CodePosition pos) {
if (annotations.isEmpty()) {
annotations = new HashMap<>();
......@@ -260,16 +270,15 @@ public class CodeWriter {
code = buf.toString();
buf = null;
Iterator<Map.Entry<CodePosition, Object>> it = annotations.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<CodePosition, Object> entry = it.next();
annotations.entrySet().removeIf(entry -> {
Object v = entry.getValue();
if (v instanceof DefinitionWrapper) {
LineAttrNode l = ((DefinitionWrapper) v).getNode();
l.setDecompiledLine(entry.getKey().getLine());
it.remove();
return true;
}
}
return false;
});
return this;
}
......
......@@ -63,6 +63,7 @@ public class InsnGen {
protected final MethodNode mth;
protected final RootNode root;
protected final boolean fallback;
protected final boolean attachInsns;
protected enum Flags {
BODY_ONLY,
......@@ -73,8 +74,9 @@ public class InsnGen {
public InsnGen(MethodGen mgen, boolean fallback) {
this.mgen = mgen;
this.mth = mgen.getMethodNode();
this.root = mth.dex().root();
this.root = mth.root();
this.fallback = fallback;
this.attachInsns = root.getArgs().isJsonOutput();
}
private boolean isFallback() {
......@@ -222,6 +224,9 @@ public class InsnGen {
} else {
if (flag != Flags.INLINE) {
code.startLineWithNum(insn.getSourceLine());
if (attachInsns) {
code.attachLineAnnotation(insn);
}
}
if (insn.getResult() != null) {
SSAVar var = insn.getResult().getSVar();
......
......@@ -85,9 +85,14 @@ public class MethodGen {
ai = ai.remove(AccessFlags.ACC_PUBLIC);
}
if (mth.getMethodInfo().isRenamed() && !ai.isConstructor()) {
if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) {
CodeGenUtils.addRenamedComment(code, mth, mth.getName());
}
CodeGenUtils.addSourceFileInfo(code, mth);
if (mth.contains(AFlag.INCONSISTENT_CODE)) {
code.startLine("/* Code decompiled incorrectly, please refer to instructions dump. */");
}
code.startLineWithNum(mth.getSourceLine());
code.add(ai.makeString());
if (Consts.DEBUG) {
......@@ -125,6 +130,15 @@ public class MethodGen {
code.add(')');
annotationGen.addThrows(mth, code);
// add default value if in annotation class
if (mth.getParentClass().getAccessFlags().isAnnotation()) {
Object def = annotationGen.getAnnotationDefaultValue(mth.getName());
if (def != null) {
code.add(" default ");
annotationGen.encodeValue(code, def);
}
}
return true;
}
......@@ -181,41 +195,49 @@ public class MethodGen {
}
public void addInstructions(CodeWriter code) throws CodegenException {
if (mth.contains(AType.JADX_ERROR)
|| mth.contains(AFlag.INCONSISTENT_CODE)
|| mth.getRegion() == null) {
code.startLine("/*");
if (mth.root().getArgs().isFallbackMode()) {
addFallbackMethodCode(code);
code.startLine("*/");
code.startLine("throw new UnsupportedOperationException(\"Method not decompiled: ")
.add(mth.getParentClass().getClassInfo().getAliasFullName())
.add('.')
.add(mth.getAlias())
.add('(')
.add(Utils.listToString(mth.getMethodInfo().getArgumentsTypes()))
.add("):")
.add(mth.getMethodInfo().getReturnType().toString())
.add("\");");
} else if (classGen.isFallbackMode()) {
dumpInstructions(code);
} else {
try {
RegionGen regionGen = new RegionGen(this);
regionGen.makeRegion(code, mth.getRegion());
} catch (StackOverflowError | BootstrapMethodError e) {
mth.addError("Method code generation error", new JadxOverflowException("StackOverflow"));
classGen.insertDecompilationProblems(code, mth);
addInstructions(code);
} catch (Exception e) {
if (mth.getParentClass().getTopParentClass().contains(AFlag.RESTART_CODEGEN)) {
throw e;
}
mth.addError("Method code generation error", e);
classGen.insertDecompilationProblems(code, mth);
addInstructions(code);
addRegionInsns(code);
}
}
public void addRegionInsns(CodeWriter code) throws CodegenException {
try {
RegionGen regionGen = new RegionGen(this);
regionGen.makeRegion(code, mth.getRegion());
} catch (StackOverflowError | BootstrapMethodError e) {
mth.addError("Method code generation error", new JadxOverflowException("StackOverflow"));
classGen.insertDecompilationProblems(code, mth);
dumpInstructions(code);
} catch (Exception e) {
if (mth.getParentClass().getTopParentClass().contains(AFlag.RESTART_CODEGEN)) {
throw e;
}
mth.addError("Method code generation error", e);
classGen.insertDecompilationProblems(code, mth);
dumpInstructions(code);
}
}
public void dumpInstructions(CodeWriter code) {
code.startLine("/*");
addFallbackMethodCode(code);
code.startLine("*/");
code.startLine("throw new UnsupportedOperationException(\"Method not decompiled: ")
.add(mth.getParentClass().getClassInfo().getAliasFullName())
.add('.')
.add(mth.getAlias())
.add('(')
.add(Utils.listToString(mth.getMethodInfo().getArgumentsTypes()))
.add("):")
.add(mth.getMethodInfo().getReturnType().toString())
.add("\");");
}
public void addFallbackMethodCode(CodeWriter code) {
if (mth.getInstructions() == null) {
// load original instructions
......@@ -244,6 +266,7 @@ public class MethodGen {
public static void addFallbackInsns(CodeWriter code, MethodNode mth, InsnNode[] insnArr, boolean addLabels) {
InsnGen insnGen = new InsnGen(getFallbackMethodGen(mth), true);
boolean attachInsns = mth.root().getArgs().isJsonOutput();
InsnNode prevInsn = null;
for (InsnNode insn : insnArr) {
if (insn == null) {
......@@ -259,6 +282,9 @@ public class MethodGen {
}
try {
code.startLine();
if (attachInsns) {
code.attachLineAnnotation(insn);
}
RegisterArg resArg = insn.getResult();
if (resArg != null) {
ArgType varType = resArg.getInitType();
......@@ -304,7 +330,7 @@ public class MethodGen {
* Return fallback variant of method codegen
*/
public static MethodGen getFallbackMethodGen(MethodNode mth) {
ClassGen clsGen = new ClassGen(mth.getParentClass(), null, true, true, true);
ClassGen clsGen = new ClassGen(mth.getParentClass(), null, false, true, true);
return new MethodGen(clsGen, mth);
}
......
......@@ -121,6 +121,17 @@ public class RegionGen extends InsnGen {
} else {
code.attachSourceLine(region.getSourceLine());
}
if (attachInsns) {
List<BlockNode> conditionBlocks = region.getConditionBlocks();
if (!conditionBlocks.isEmpty()) {
BlockNode blockNode = conditionBlocks.get(0);
InsnNode lastInsn = BlockUtils.getLastInsn(blockNode);
if (lastInsn != null) {
code.attachLineAnnotation(lastInsn);
}
}
}
code.add("if (");
new ConditionGen(this).add(code, region.getCondition());
code.add(") {");
......@@ -128,7 +139,7 @@ public class RegionGen extends InsnGen {
code.startLine('}');
IContainer els = region.getElseRegion();
if (els != null && RegionUtils.notEmpty(els)) {
if (RegionUtils.notEmpty(els)) {
code.add(" else ");
if (connectElseIf(code, els)) {
return;
......
package jadx.core.codegen.json;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jadx.api.CodePosition;
import jadx.api.JadxArgs;
import jadx.core.codegen.ClassGen;
import jadx.core.codegen.CodeWriter;
import jadx.core.codegen.MethodGen;
import jadx.core.codegen.json.cls.JsonClass;
import jadx.core.codegen.json.cls.JsonCodeLine;
import jadx.core.codegen.json.cls.JsonField;
import jadx.core.codegen.json.cls.JsonMethod;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class JsonCodeGen {
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
.disableHtmlEscaping()
.create();
private final ClassNode cls;
private final JadxArgs args;
private final RootNode root;
public JsonCodeGen(ClassNode cls) {
this.cls = cls;
this.root = cls.root();
this.args = root.getArgs();
}
public String process() {
JsonClass jsonCls = processCls(cls, null);
return GSON.toJson(jsonCls);
}
private JsonClass processCls(ClassNode cls, @Nullable ClassGen parentCodeGen) {
ClassGen classGen;
if (parentCodeGen == null) {
classGen = new ClassGen(cls, args);
} else {
classGen = new ClassGen(cls, parentCodeGen);
}
ClassInfo classInfo = cls.getClassInfo();
JsonClass jsonCls = new JsonClass();
jsonCls.setPkg(classInfo.getAliasPkg());
jsonCls.setDex(cls.dex().getDexFile().getName());
jsonCls.setName(classInfo.getFullName());
if (classInfo.hasAlias()) {
jsonCls.setAlias(classInfo.getAliasFullName());
}
jsonCls.setType(getClassTypeStr(cls));
jsonCls.setAccessFlags(cls.getAccessFlags().rawValue());
if (!Objects.equals(cls.getSuperClass(), ArgType.OBJECT)) {
jsonCls.setSuperClass(getTypeAlias(cls.getSuperClass()));
}
if (!cls.getInterfaces().isEmpty()) {
jsonCls.setInterfaces(Utils.collectionMap(cls.getInterfaces(), this::getTypeAlias));
}
CodeWriter cw = new CodeWriter();
CodeGenUtils.addComments(cw, cls);
classGen.insertDecompilationProblems(cw, cls);
classGen.addClassDeclaration(cw);
jsonCls.setDeclaration(cw.finish().toString());
addFields(cls, jsonCls, classGen);
addMethods(cls, jsonCls, classGen);
addInnerClasses(cls, jsonCls, classGen);
if (!cls.getClassInfo().isInner()) {
List<String> imports = Utils.collectionMap(classGen.getImports(), ClassInfo::getAliasFullName);
Collections.sort(imports);
jsonCls.setImports(imports);
}
return jsonCls;
}
private void addInnerClasses(ClassNode cls, JsonClass jsonCls, ClassGen classGen) {
List<ClassNode> innerClasses = cls.getInnerClasses();
if (innerClasses.isEmpty()) {
return;
}
jsonCls.setInnerClasses(new ArrayList<>(innerClasses.size()));
for (ClassNode innerCls : innerClasses) {
if (innerCls.contains(AFlag.DONT_GENERATE)) {
continue;
}
JsonClass innerJsonCls = processCls(innerCls, classGen);
jsonCls.getInnerClasses().add(innerJsonCls);
}
}
private void addFields(ClassNode cls, JsonClass jsonCls, ClassGen classGen) {
jsonCls.setFields(new ArrayList<>());
for (FieldNode field : cls.getFields()) {
if (field.contains(AFlag.DONT_GENERATE)) {
continue;
}
JsonField jsonField = new JsonField();
jsonField.setName(field.getName());
if (field.getFieldInfo().hasAlias()) {
jsonField.setAlias(field.getAlias());
}
CodeWriter cw = new CodeWriter();
classGen.addField(cw, field);
jsonField.setDeclaration(cw.finish().toString());
jsonField.setAccessFlags(field.getAccessFlags().rawValue());
jsonCls.getFields().add(jsonField);
}
}
private void addMethods(ClassNode cls, JsonClass jsonCls, ClassGen classGen) {
jsonCls.setMethods(new ArrayList<>());
for (MethodNode mth : cls.getMethods()) {
if (mth.contains(AFlag.DONT_GENERATE)) {
continue;
}
JsonMethod jsonMth = new JsonMethod();
jsonMth.setName(mth.getName());
if (mth.getMethodInfo().hasAlias()) {
jsonMth.setAlias(mth.getAlias());
}
jsonMth.setSignature(mth.getMethodInfo().getShortId());
jsonMth.setReturnType(getTypeAlias(mth.getReturnType()));
jsonMth.setArguments(Utils.collectionMap(mth.getMethodInfo().getArgumentsTypes(), this::getTypeAlias));
MethodGen mthGen = new MethodGen(classGen, mth);
CodeWriter cw = new CodeWriter();
mthGen.addDefinition(cw);
jsonMth.setDeclaration(cw.finish().toString());
jsonMth.setAccessFlags(mth.getAccessFlags().rawValue());
jsonMth.setLines(fillMthCode(mth, mthGen));
jsonMth.setOffset("0x" + Long.toHexString(mth.getMethodCodeOffset()));
jsonCls.getMethods().add(jsonMth);
}
}
private List<JsonCodeLine> fillMthCode(MethodNode mth, MethodGen mthGen) {
if (mth.isNoCode()) {
return Collections.emptyList();
}
CodeWriter code = new CodeWriter();
try {
mthGen.addInstructions(code);
} catch (Exception e) {
throw new JadxRuntimeException("Method generation error", e);
}
code.finish();
String codeStr = code.toString();
if (codeStr.isEmpty()) {
return Collections.emptyList();
}
String[] lines = codeStr.split(CodeWriter.NL);
Map<Integer, Integer> lineMapping = code.getLineMapping();
Map<CodePosition, Object> annotations = code.getAnnotations();
long mthCodeOffset = mth.getMethodCodeOffset() + 16;
int linesCount = lines.length;
List<JsonCodeLine> codeLines = new ArrayList<>(linesCount);
for (int i = 0; i < linesCount; i++) {
String codeLine = lines[i];
int line = i + 2;
JsonCodeLine jsonCodeLine = new JsonCodeLine();
jsonCodeLine.setCode(codeLine);
jsonCodeLine.setSourceLine(lineMapping.get(line));
Object obj = annotations.get(new CodePosition(line, 0));
if (obj instanceof InsnNode) {
int offset = ((InsnNode) obj).getOffset();
jsonCodeLine.setOffset("0x" + Long.toHexString(mthCodeOffset + offset * 2));
}
codeLines.add(jsonCodeLine);
}
return codeLines;
}
private String getTypeAlias(ArgType clsType) {
if (Objects.equals(clsType, ArgType.OBJECT)) {
return ArgType.OBJECT.getObject();
}
if (clsType.isObject()) {
ClassInfo classInfo = ClassInfo.fromType(root, clsType);
return classInfo.getAliasFullName();
}
return clsType.toString();
}
private String getClassTypeStr(ClassNode cls) {
if (cls.isEnum()) {
return "enum";
}
if (cls.getAccessFlags().isInterface()) {
return "interface";
}
return "class";
}
}
package jadx.core.codegen.json;
import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jadx.api.JadxArgs;
import jadx.core.codegen.json.mapping.JsonClsMapping;
import jadx.core.codegen.json.mapping.JsonFieldMapping;
import jadx.core.codegen.json.mapping.JsonMapping;
import jadx.core.codegen.json.mapping.JsonMthMapping;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
public class JsonMappingGen {
private static final Logger LOG = LoggerFactory.getLogger(JsonMappingGen.class);
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES)
.disableHtmlEscaping()
.create();
public static void dump(RootNode root) {
JsonMapping mapping = new JsonMapping();
fillMapping(mapping, root);
JadxArgs args = root.getArgs();
File outDirSrc = args.getOutDirSrc().getAbsoluteFile();
File mappingFile = new File(outDirSrc, "mapping.json");
FileUtils.makeDirsForFile(mappingFile);
try (Writer writer = new FileWriter(mappingFile)) {
GSON.toJson(mapping, writer);
LOG.info("Save mappings to {}", mappingFile.getAbsolutePath());
} catch (Exception e) {
throw new JadxRuntimeException("Failed to save mapping json", e);
}
}
private static void fillMapping(JsonMapping mapping, RootNode root) {
List<ClassNode> classes = root.getClasses(true);
mapping.setClasses(new ArrayList<>(classes.size()));
for (ClassNode cls : classes) {
ClassInfo classInfo = cls.getClassInfo();
JsonClsMapping jsonCls = new JsonClsMapping();
jsonCls.setName(classInfo.getRawName());
jsonCls.setAlias(classInfo.getAliasFullName());
jsonCls.setInner(classInfo.isInner());
jsonCls.setJson(cls.getTopParentClass().getClassInfo().getAliasFullPath() + ".json");
if (classInfo.isInner()) {
jsonCls.setTopClass(cls.getTopParentClass().getClassInfo().getFullName());
}
addFields(cls, jsonCls);
addMethods(cls, jsonCls);
mapping.getClasses().add(jsonCls);
}
}
private static void addMethods(ClassNode cls, JsonClsMapping jsonCls) {
List<MethodNode> methods = cls.getMethods();
if (methods.isEmpty()) {
return;
}
jsonCls.setMethods(new ArrayList<>(methods.size()));
for (MethodNode method : methods) {
JsonMthMapping jsonMethod = new JsonMthMapping();
MethodInfo methodInfo = method.getMethodInfo();
jsonMethod.setSignature(methodInfo.getShortId());
jsonMethod.setName(methodInfo.getName());
jsonMethod.setAlias(methodInfo.getAlias());
jsonMethod.setOffset("0x" + Long.toHexString(method.getMethodCodeOffset()));
jsonCls.getMethods().add(jsonMethod);
}
}
private static void addFields(ClassNode cls, JsonClsMapping jsonCls) {
List<FieldNode> fields = cls.getFields();
if (fields.isEmpty()) {
return;
}
jsonCls.setFields(new ArrayList<>(fields.size()));
for (FieldNode field : fields) {
JsonFieldMapping jsonField = new JsonFieldMapping();
jsonField.setName(field.getName());
jsonField.setAlias(field.getAlias());
jsonCls.getFields().add(jsonField);
}
}
private JsonMappingGen() {
}
}
package jadx.core.codegen.json.cls;
import java.util.List;
import com.google.gson.annotations.SerializedName;
public class JsonClass extends JsonNode {
@SerializedName("package")
private String pkg;
private String type; // class, interface, enum
@SerializedName("extends")
private String superClass;
@SerializedName("implements")
private List<String> interfaces;
private String dex;
private List<JsonField> fields;
private List<JsonMethod> methods;
private List<JsonClass> innerClasses;
private List<String> imports;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getSuperClass() {
return superClass;
}
public void setSuperClass(String superClass) {
this.superClass = superClass;
}
public List<String> getInterfaces() {
return interfaces;
}
public void setInterfaces(List<String> interfaces) {
this.interfaces = interfaces;
}
public List<JsonField> getFields() {
return fields;
}
public void setFields(List<JsonField> fields) {
this.fields = fields;
}
public List<JsonMethod> getMethods() {
return methods;
}
public void setMethods(List<JsonMethod> methods) {
this.methods = methods;
}
public List<JsonClass> getInnerClasses() {
return innerClasses;
}
public void setInnerClasses(List<JsonClass> innerClasses) {
this.innerClasses = innerClasses;
}
public String getPkg() {
return pkg;
}
public void setPkg(String pkg) {
this.pkg = pkg;
}
public String getDex() {
return dex;
}
public void setDex(String dex) {
this.dex = dex;
}
public List<String> getImports() {
return imports;
}
public void setImports(List<String> imports) {
this.imports = imports;
}
}
package jadx.core.codegen.json.cls;
import org.jetbrains.annotations.Nullable;
public class JsonCodeLine {
private String code;
private String offset;
private Integer sourceLine;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getOffset() {
return offset;
}
public void setOffset(String offset) {
this.offset = offset;
}
public Integer getSourceLine() {
return sourceLine;
}
public void setSourceLine(@Nullable Integer sourceLine) {
this.sourceLine = sourceLine;
}
}
package jadx.core.codegen.json.cls;
public class JsonField extends JsonNode {
String type;
}
package jadx.core.codegen.json.cls;
import java.util.List;
public class JsonMethod extends JsonNode {
private String signature;
private String returnType;
private List<String> arguments;
private List<JsonCodeLine> lines;
private String offset;
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
public String getReturnType() {
return returnType;
}
public void setReturnType(String returnType) {
this.returnType = returnType;
}
public List<String> getArguments() {
return arguments;
}
public void setArguments(List<String> arguments) {
this.arguments = arguments;
}
public List<JsonCodeLine> getLines() {
return lines;
}
public void setLines(List<JsonCodeLine> lines) {
this.lines = lines;
}
public String getOffset() {
return offset;
}
public void setOffset(String offset) {
this.offset = offset;
}
}
package jadx.core.codegen.json.cls;
public class JsonNode {
private String name;
private String alias;
private String declaration;
private int accessFlags;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getDeclaration() {
return declaration;
}
public void setDeclaration(String declaration) {
this.declaration = declaration;
}
public int getAccessFlags() {
return accessFlags;
}
public void setAccessFlags(int accessFlags) {
this.accessFlags = accessFlags;
}
}
package jadx.core.codegen.json.mapping;
import java.util.List;
public class JsonClsMapping {
private String name;
private String alias;
private String json;
private boolean inner;
private String topClass;
private List<JsonFieldMapping> fields;
private List<JsonMthMapping> methods;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getJson() {
return json;
}
public void setJson(String json) {
this.json = json;
}
public boolean isInner() {
return inner;
}
public void setInner(boolean inner) {
this.inner = inner;
}
public String getTopClass() {
return topClass;
}
public void setTopClass(String topClass) {
this.topClass = topClass;
}
public List<JsonFieldMapping> getFields() {
return fields;
}
public void setFields(List<JsonFieldMapping> fields) {
this.fields = fields;
}
public List<JsonMthMapping> getMethods() {
return methods;
}
public void setMethods(List<JsonMthMapping> methods) {
this.methods = methods;
}
}
package jadx.core.codegen.json.mapping;
public class JsonFieldMapping {
private String name;
private String alias;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
}
package jadx.core.codegen.json.mapping;
import java.util.List;
public class JsonMapping {
private List<JsonClsMapping> classes;
public List<JsonClsMapping> getClasses() {
return classes;
}
public void setClasses(List<JsonClsMapping> classes) {
this.classes = classes;
}
}
package jadx.core.codegen.json.mapping;
public class JsonMthMapping {
private String signature;
private String name;
private String alias;
private String offset;
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getOffset() {
return offset;
}
public void setOffset(String offset) {
this.offset = offset;
}
}
......@@ -142,7 +142,7 @@ public class Deobfuscator {
}
for (MethodInfo mth : o.getMethods()) {
if (aliasToUse == null) {
if (mth.isRenamed() && !mth.isAliasFromPreset()) {
if (mth.hasAlias() && !mth.isAliasFromPreset()) {
mth.setAlias(String.format("mo%d%s", id, prepareNamePart(mth.getName())));
}
aliasToUse = mth.getAlias();
......
......@@ -201,6 +201,10 @@ public class AccessInfo {
}
}
public int rawValue() {
return accFlags;
}
@Override
public String toString() {
return "AccessInfo: " + type + " 0x" + Integer.toHexString(accFlags) + " (" + rawString() + ')';
......
package jadx.core.dex.info;
import java.util.Objects;
import com.android.dex.FieldId;
import jadx.core.codegen.TypeGen;
......@@ -53,6 +55,10 @@ public final class FieldInfo {
this.alias = alias;
}
public boolean hasAlias() {
return !Objects.equals(name, alias);
}
public String getFullId() {
return declClass.getFullName() + '.' + name + ':' + TypeGen.signature(type);
}
......
......@@ -130,7 +130,7 @@ public final class MethodInfo {
this.alias = alias;
}
public boolean isRenamed() {
public boolean hasAlias() {
return !name.equals(alias);
}
......
......@@ -667,6 +667,10 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode {
return mthInfo;
}
public long getMethodCodeOffset() {
return noCode ? 0 : methodData.getCodeOffset();
}
/**
* Stat method.
* Calculate instructions count as a measure of method size
......
package jadx.core.dex.visitors;
import jadx.core.codegen.json.JsonMappingGen;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.utils.exceptions.JadxException;
public class FallbackModeVisitor extends AbstractVisitor {
@Override
public void init(RootNode root) {
if (root.getArgs().isJsonOutput()) {
JsonMappingGen.dump(root);
}
}
@Override
public void visit(MethodNode mth) throws JadxException {
if (mth.isNoCode()) {
return;
......
......@@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable;
import jadx.api.JadxArgs;
import jadx.core.Consts;
import jadx.core.codegen.json.JsonMappingGen;
import jadx.core.deobf.Deobfuscator;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
......@@ -51,6 +52,9 @@ public class RenameVisitor extends AbstractVisitor {
deobfuscator.savePresets();
deobfuscator.clear();
}
if (args.isJsonOutput()) {
JsonMappingGen.dump(root);
}
}
private static void checkClasses(Deobfuscator deobfuscator, RootNode root, JadxArgs args) {
......
......@@ -13,7 +13,7 @@ public class SaveCode {
private SaveCode() {
}
public static void save(File dir, JadxArgs args, ClassNode cls) {
public static void save(File dir, ClassNode cls) {
if (cls.contains(AFlag.DONT_GENERATE)) {
return;
}
......@@ -21,10 +21,24 @@ public class SaveCode {
if (clsCode == null) {
throw new JadxRuntimeException("Code not generated for class " + cls.getFullName());
}
String fileName = cls.getClassInfo().getAliasFullPath() + ".java";
if (args.isFallbackMode()) {
fileName += ".jadx";
if (clsCode == CodeWriter.EMPTY) {
return;
}
String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls);
clsCode.save(dir, fileName);
}
private static String getFileExtension(ClassNode cls) {
JadxArgs.OutputFormatEnum outputFormat = cls.root().getArgs().getOutputFormat();
switch (outputFormat) {
case JAVA:
return ".java";
case JSON:
return ".json";
default:
throw new JadxRuntimeException("Unknown output format: " + outputFormat);
}
}
}
......@@ -6,6 +6,7 @@ import jadx.core.codegen.CodeWriter;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.AttrNode;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.attributes.nodes.SourceFileAttr;
public class CodeGenUtils {
......@@ -27,6 +28,13 @@ public class CodeGenUtils {
code.add(" */");
}
public static void addSourceFileInfo(CodeWriter code, AttrNode node) {
SourceFileAttr sourceFileAttr = node.get(AType.SOURCE_FILE);
if (sourceFileAttr != null) {
code.startLine("/* compiled from: ").add(sourceFileAttr.getFileName()).add(" */");
}
}
private CodeGenUtils() {
}
}
......@@ -210,11 +210,26 @@ public final class ImmutableList<E> implements List<E>, RandomAccess {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
if (o instanceof ImmutableList) {
ImmutableList<?> other = (ImmutableList<?>) o;
return Arrays.equals(arr, other.arr);
}
ImmutableList<?> that = (ImmutableList<?>) o;
return Arrays.equals(arr, that.arr);
if (o instanceof List) {
List<?> other = (List<?>) o;
int size = size();
if (size != other.size()) {
return false;
}
for (int i = 0; i < size; i++) {
E e1 = arr[i];
Object e2 = other.get(i);
if (!Objects.equals(e1, e2)) {
return false;
}
}
return true;
}
return false;
}
@Override
......
......@@ -3,7 +3,9 @@ package jadx.core.utils;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
......@@ -154,6 +156,17 @@ public class Utils {
}
}
public static <T, R> List<R> collectionMap(Collection<T> list, Function<T, R> mapFunc) {
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
List<R> result = new ArrayList<>(list.size());
for (T t : list) {
result.add(mapFunc.apply(t));
}
return result;
}
public static <T> List<T> lockList(List<T> list) {
if (list.isEmpty()) {
return Collections.emptyList();
......
......@@ -97,7 +97,7 @@ public class InputFile {
}
private void addDexFile(Path path) throws IOException {
addDexFile("", path);
addDexFile(path.getFileName().toString(), path);
}
private void addDexFile(String fileName, Path path) throws IOException {
......
package jadx.tests.integration.others;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.api.JadxArgs;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.JadxMatchers.containsOne;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
public class TestJsonOutput extends IntegrationTest {
public static class TestCls {
private final String prefix = "list: ";
static {
System.out.println("test");
}
public void test(boolean b, List<String> list) {
if (b) {
System.out.println(prefix + list);
}
}
public static class Inner implements Runnable {
@Override
public void run() {
System.out.println("run");
}
}
}
@Test
public void test() {
disableCompilation();
args.setOutputFormat(JadxArgs.OutputFormatEnum.JSON);
ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString();
assertThat(code, containsString("\"offset\": \"0x"));
assertThat(code, containsOne("public static class Inner implements Runnable"));
}
@Test
public void testFallback() {
disableCompilation();
setFallback();
args.setOutputFormat(JadxArgs.OutputFormatEnum.JSON);
ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString();
assertThat(code, containsString("\"offset\": \"0x"));
assertThat(code, containsOne("public static class Inner implements java.lang.Runnable"));
}
}
......@@ -15,7 +15,6 @@ dependencies {
compile(project(":jadx-cli"))
compile 'com.fifesoft:rsyntaxtextarea:3.0.2'
compile 'com.google.code.gson:gson:2.8.5'
compile files('libs/jfontchooser-1.0.5.jar')
compile 'hu.kazocsaba:image-viewer:1.2.3'
......
package jadx.gui.settings;
import java.awt.Font;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Window;
import java.awt.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
......@@ -15,7 +12,7 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import javax.swing.JFrame;
import javax.swing.*;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.jetbrains.annotations.Nullable;
......@@ -42,7 +39,9 @@ public class JadxSettings extends JadxCLIArgs {
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
static final Set<String> SKIP_FIELDS = new HashSet<>(Arrays.asList(
"files", "input", "outDir", "outDirSrc", "outDirRes", "verbose", "printVersion", "printHelp"));
"files", "input", "outDir", "outDirSrc", "outDirRes", "outputFormat",
"verbose", "printVersion", "printHelp"));
private Path lastSaveProjectPath = USER_HOME;
private Path lastOpenFilePath = USER_HOME;
private Path lastSaveFilePath = USER_HOME;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment