diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d659595a..2bea8b862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to this project will be documented in this file. - [PR109] FLA export - large font size of DefineEditText - [PR110] FLA export - image instances - FLA export - missing AS linkage class for fonts +- [#2077] Switch detection problems producing §§goto ### Changed - [#2070] String values inside SWF to XML export are backslash escaped @@ -3083,6 +3084,7 @@ Major version of SWF to XML export changed to 2. [#2058]: https://www.free-decompiler.com/flash/issues/2058 [#2072]: https://www.free-decompiler.com/flash/issues/2072 [#2029]: https://www.free-decompiler.com/flash/issues/2029 +[#2077]: https://www.free-decompiler.com/flash/issues/2077 [#1998]: https://www.free-decompiler.com/flash/issues/1998 [#2038]: https://www.free-decompiler.com/flash/issues/2038 [#2028]: https://www.free-decompiler.com/flash/issues/2028 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/Graph.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/Graph.java index 291587a30..61ccc6042 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/Graph.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/Graph.java @@ -55,12 +55,15 @@ import com.jpexs.decompiler.graph.model.UniversalLoopItem; import com.jpexs.decompiler.graph.model.WhileItem; import com.jpexs.decompiler.graph.precontinues.GraphPrecontinueDetector; import com.jpexs.helpers.Reference; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -121,7 +124,94 @@ public class Graph { for (GraphPart head : heads) { time = head.setTime(time, ordered, visited); head.setNumblocks(1); + } + } + + /** + * Calculates time of closing the node. + * The node is closed when all its input edges are already visited + * (not counting back edges), then all its output edges are processed. + * + * This time is useful when sorting nodes according their occurence + * in getMostCommonPart method - used for switch detection + * + * @param loops Already calculated loops to get backedges from. + */ + private void calculateClosedTime(List loops) { + ArrayDeque openedNodes = new ArrayDeque<>(); + Set closedNodes = new HashSet<>(); + Set visitedEdges = new HashSet<>(); + openedNodes.addAll(heads); + for(GraphPart h:heads) { + for (GraphPart r:h.refs) { + visitedEdges.add(new LevelMapEdge(r, h)); + } } + for (Loop el:loops) { + for (GraphPart be:el.backEdges) { + visitedEdges.add(new LevelMapEdge(be, el.loopContinue)); + } + } + + int closedTime = 1; + + loopopened: while (!openedNodes.isEmpty()) { + GraphPart part = openedNodes.remove(); + if (closedNodes.contains(part)) { + continue; + } + for (GraphPart r:part.refs) { + if (!visitedEdges.contains(new LevelMapEdge(r, part))) { + continue loopopened; + } + } + for (GraphPart n:part.nextParts) { + openedNodes.add(n); + visitedEdges.add(new LevelMapEdge(part, n)); + } + closedNodes.add(part); + part.closedTime = closedTime++; + //System.err.println("part "+part+" closedTime: "+part.closedTime); + } + + } + + private class LevelMapEdge { + public GraphPart from; + public GraphPart to; + + public LevelMapEdge(GraphPart from, GraphPart to) { + this.from = from; + this.to = to; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 31 * hash + Objects.hashCode(this.from); + hash = 31 * hash + Objects.hashCode(this.to); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final LevelMapEdge other = (LevelMapEdge) obj; + //use == comparison, not equals, as some parts may be equal + // (the refs to throw targets have -1,-1 as start/end) + if (this.from != other.from) { + return false; + } + return this.to == other.to; + } } public List getExceptions() { @@ -377,7 +467,31 @@ public class Graph { allReachable.add(p); allReachable.addAll(r1); } + Comparator comparator = new Comparator() { + @Override + public int compare(PartCommon o1, PartCommon o2) { + int levelCompare = o2.level - o1.level; + if (levelCompare == 0) { + try { + if (o1.part.leadsTo(localData, Graph.this, code, o2.part, loops, throwStates, false)) { + return -1; + } + if (o2.part.leadsTo(localData, Graph.this, code, o1.part, loops, throwStates, false)) { + return 1; + } + return 0; + } catch (InterruptedException ex) { + //ignore + return 0; + } + //return o1.part.discoveredTime - o2.part.discoveredTime; + } else { + return levelCompare; + } + } + }; Set commonSet = new TreeSet<>(); + for (GraphPart r : allReachable) { if (loopContinues.contains(r)) { continue; @@ -496,7 +610,12 @@ public class Graph { return null; } if (debugPrintLoopList) { - System.err.println("selected " + pc.part); + StringBuilder sb = new StringBuilder(); + for (GraphPart p : parts) { + sb.append(" "); + sb.append(p.toString()); + } + System.err.println("most common part of" + sb.toString() + " is " + pc.part); } return pc.part; } @@ -504,7 +623,7 @@ public class Graph { return null; } - private class PartCommon implements Comparable { + private class PartCommon implements Comparable { public GraphPart part; public int level; @@ -518,7 +637,7 @@ public class Graph { public int compareTo(PartCommon o) { int ret = o.level - level; if (ret == 0) { - ret = part.start - o.part.start; + ret = part.closedTime - o.part.closedTime; } return ret; } @@ -637,6 +756,7 @@ public class Graph { //TODO: Make getPrecontinues faster getBackEdges(localData, loops, throwStates); + calculateClosedTime(loops); new GraphPrecontinueDetector().detectPrecontinues(heads, allParts, loops, throwStates); if (debugPrintLoopList) { @@ -1732,16 +1852,16 @@ public class Graph { getLoopsWalk(localData, next, loops, throwStates, stopPart, false, visited, level); } } else if (part.nextParts.size() == 1) { - + if (!isLoop || currentLoop == null) { part = part.nextParts.get(0); first = false; continue loopwalk; //to avoid recursion - } else { + } else { getLoopsWalk(localData, part.nextParts.get(0), loops, throwStates, stopPart, false, visited, level); } } - + if (isLoop && currentLoop != null) { GraphPart found; @@ -2350,12 +2470,12 @@ public class Graph { List iiOnTrue = ii.onTrue; List iiOnFalse = ii.onFalse; if ((ii.expression instanceof EqualsTypeItem) || (ii.expression instanceof NotEqualsTypeItem)) { - + if (ii.expression instanceof NotEqualsTypeItem) { iiOnTrue = ii.onFalse; iiOnFalse = ii.onTrue; } - + if (!iiOnFalse.isEmpty() && !iiOnTrue.isEmpty() && iiOnTrue.get(iiOnTrue.size() - 1) instanceof PushItem && iiOnTrue.get(iiOnTrue.size() - 1).value instanceof IntegerValueTypeItem) { @@ -2390,7 +2510,7 @@ public class Graph { toOnTrue = to.onFalse; toOnFalse = to.onTrue; } - + if (toOnTrue instanceof IntegerValueTypeItem) { int cpos = ((IntegerValueTypeItem) toOnTrue).intValue(); caseExpressionLeftSides.put(cpos, ((BinaryOpItem) to.expression).getLeftSide()); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/GraphPart.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/GraphPart.java index 79b386e7b..eb1f7062a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/GraphPart.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/graph/GraphPart.java @@ -72,6 +72,8 @@ public class GraphPart implements Serializable { public int discoveredTime; public int finishedTime; + + public int closedTime; public int order; diff --git a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3AssembledDecompileTest.java b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3AssembledDecompileTest.java index 9015f9d6a..36d7c8be5 100644 --- a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3AssembledDecompileTest.java +++ b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3AssembledDecompileTest.java @@ -330,6 +330,33 @@ public class ActionScript3AssembledDecompileTest extends ActionScript3DecompileT false); } + @Test + public void testSwitchMostCommon() { + decompileMethod("assembled", "testSwitchMostCommon", "var _loc2_:int = 0;\r\n" + + "var _loc4_:* = undefined;\r\n" + + "if(something == null)\r\n" + + "{\r\n" + + "switch(param1.keyCode)\r\n" + + "{\r\n" + + "case 89:\r\n" + + "_loc2_ = 7;\r\n" + + "break;\r\n" + + "case 112:\r\n" + + "return;\r\n" + + "}\r\n" + + "switch(param1.charCode)\r\n" + + "{\r\n" + + "case 49:\r\n" + + "return;\r\n" + + "case 69:\r\n" + + "return;\r\n" + + "case 113:\r\n" + + "_loc2_ = 1;\r\n" + + "}\r\n" + + "}\r\n", + false); + } + @Test public void testTryCatchLoopBreakLevel2() { decompileMethod("assembled", "testTryCatchLoopBreakLevel2", "var a:int = 0;\r\n" diff --git a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/generators/AS3Generator.java b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/generators/AS3Generator.java index 8a98bfc70..572ec557b 100644 --- a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/generators/AS3Generator.java +++ b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/generators/AS3Generator.java @@ -61,6 +61,22 @@ public class AS3Generator { for (ScriptPack pack : scriptPacks) { sortedPacks.put(pack.getClassPath().toRawString(), pack); } + s.append("/*\r\n" + + " * Copyright (C) 2010-2023 JPEXS, All rights reserved.\r\n" + + " * \r\n" + + " * This library is free software; you can redistribute it and/or\r\n" + + " * modify it under the terms of the GNU Lesser General Public\r\n" + + " * License as published by the Free Software Foundation; either\r\n" + + " * version 3.0 of the License, or (at your option) any later version.\r\n" + + " * \r\n" + + " * This library is distributed in the hope that it will be useful,\r\n" + + " * but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n" + + " * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\r\n" + + " * Lesser General Public License for more details.\r\n" + + " * \r\n" + + " * You should have received a copy of the GNU Lesser General Public\r\n" + + " * License along with this library.\r\n" + + " */\r\n"); s.append("package com.jpexs.decompiler.flash.as3decompile;\r\n"); s.append("\r\n"); s.append("import com.jpexs.decompiler.flash.ActionScript3DecompileTestBase;\r\n"); @@ -142,12 +158,12 @@ public class AS3Generator { MethodBody b = abc.findBody(((TraitMethodGetterSetter) t).method_info); List ts = new ArrayList<>(); ts.add(abc.instance_info.get(classId).instance_traits); - + Configuration.autoDeobfuscate.set(clsName.toLowerCase().contains("obfus")); - + List callStack = new ArrayList<>(); callStack.add(b); - b.convert(callStack, swf.getAbcIndex(),new ConvertData(), "", ScriptExportMode.AS, false, ((TraitMethodGetterSetter) t).method_info, pack.scriptIndex, classId, abc, null, new ScopeStack(), 0, new NulWriter(), new ArrayList<>(), ts, true, new HashSet<>()); + b.convert(callStack, swf.getAbcIndex(), new ConvertData(), "", ScriptExportMode.AS, false, ((TraitMethodGetterSetter) t).method_info, pack.scriptIndex, classId, abc, null, new ScopeStack(), 0, new NulWriter(), new ArrayList<>(), ts, true, new HashSet<>()); b.toString(callStack, swf.getAbcIndex(), "", ScriptExportMode.AS, abc, null, src, new ArrayList<>(), new HashSet<>()); String[] srcs = src.toString().split("[\r\n]+"); for (int i = 0; i < srcs.length; i++) { @@ -179,7 +195,6 @@ public class AS3Generator { Configuration.simplifyExpressions.set(false); Configuration.displayDupInstructions.set(true); - useFile("ActionScript3ClassicDecompileTest", new String[][]{{"testdata/as3_new/bin/as3_new.flex.swf", "classic"}}, false); useFile("ActionScript3ClassicAirDecompileTest", new String[][]{{"testdata/as3_new/bin/as3_new.air.swf", "classic_air"}}, false); diff --git a/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.abc b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.abc index 12597eff2..dcd6565ef 100644 Binary files a/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.abc and b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.abc differ diff --git a/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.asasm b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.asasm index a5f471b2d..5f2dcc9d5 100644 --- a/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.asasm +++ b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/as3_assembled-0.main.asasm @@ -32,5 +32,6 @@ program #include "tests/TestActivationProps.script.asasm" #include "tests/TestSwapAssignment.script.asasm" #include "tests/TestMutatingSwitch.script.asasm" + #include "tests/TestSwitchMostCommon.script.asasm" ; place to add next end ; program diff --git a/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/tests/TestSwitchMostCommon.class.asasm b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/tests/TestSwitchMostCommon.class.asasm new file mode 100644 index 000000000..8a61aff46 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/tests/TestSwitchMostCommon.class.asasm @@ -0,0 +1,145 @@ +class + refid "tests:TestSwitchMostCommon" + instance QName(PackageNamespace("tests"), "TestSwitchMostCommon") + extends QName(PackageNamespace(""), "Object") + flag SEALED + flag PROTECTEDNS + protectedns ProtectedNamespace("tests:TestSwitchMostCommon") + iinit + refid "tests:TestSwitchMostCommon/instance/init" + body + maxstack 1 + localcount 1 + initscopedepth 4 + maxscopedepth 5 + code + getlocal0 + pushscope + + getlocal0 + constructsuper 0 + + returnvoid + end ; code + end ; body + end ; method + trait method QName(PackageNamespace(""), "run") + method + refid "tests:TestSwitchMostCommon/instance/run" + param QName(PackageNamespace("flash.events"),"KeyboardEvent") + returns QName(PackageNamespace(""), "void") + body + maxstack 10 + localcount 8 + initscopedepth 10 + maxscopedepth 11 + + code + getlocal0 + pushscope + pushbyte 0 + setlocal2 + pushundefined + coerce_a + setlocal 4 + getlex Multiname("something",[PrivateNamespace("*","111"),PackageNamespace("","9369"),PrivateNamespace("*","412"),PackageInternalNs(""),Namespace("http://adobe.com/AS3/2006/builtin"),PackageNamespace("flash.text"),PackageNamespace("flash.external"),PackageNamespace("it.gotoandplay.smartfoxserver"),Namespace("com.facebook.graph"),ProtectedNamespace("Game"),StaticProtectedNs("Game"),StaticProtectedNs("flash.display:MovieClip"),StaticProtectedNs("flash.display:Sprite"),StaticProtectedNs("flash.display:DisplayObjectContainer"),StaticProtectedNs("flash.display:InteractiveObject"),StaticProtectedNs("flash.display:DisplayObject"),StaticProtectedNs("flash.events:EventDispatcher")]) + pushnull + equals + iffalse ofs00b5 + jump ofs0026 + ofs0017: + label + pushbyte 7 + setlocal2 + jump ofs006f + ofs001f: + label + returnvoid + ofs0021: + label + jump ofs006f + ofs0026: + getlocal1 + getproperty Multiname("keyCode",[PrivateNamespace("*","111"),PackageNamespace("","9369"),PrivateNamespace("*","412"),PackageInternalNs(""),Namespace("http://adobe.com/AS3/2006/builtin"),PackageNamespace("flash.text"),PackageNamespace("flash.external"),PackageNamespace("it.gotoandplay.smartfoxserver"),Namespace("com.facebook.graph"),ProtectedNamespace("Game"),StaticProtectedNs("Game"),StaticProtectedNs("flash.display:MovieClip"),StaticProtectedNs("flash.display:Sprite"),StaticProtectedNs("flash.display:DisplayObjectContainer"),StaticProtectedNs("flash.display:InteractiveObject"),StaticProtectedNs("flash.display:DisplayObject"),StaticProtectedNs("flash.events:EventDispatcher")]) + setlocal 5 + pushbyte 89 + getlocal 5 + ifstrictne ofs003a + pushbyte 0 + jump ofs004a + ofs003a: + pushbyte 112 + getlocal 5 + ifstrictne ofs0048 + pushbyte 1 + jump ofs004a + ofs0048: + pushbyte 2 + ofs004a: + kill 5 + lookupswitch ofs0021, [ofs0017, ofs001f, ofs0021] + ofs005a: + label + returnvoid + ofs005c: + label + returnvoid + ofs005e: + label + pushbyte 1 + setlocal2 + jump ofs006b + ofs0066: + label + jump ofs006b + ofs006b: + jump ofs00b5 + ofs006f: + getlocal1 + getproperty Multiname("charCode",[PrivateNamespace("*","111"),PackageNamespace("","9369"),PrivateNamespace("*","412"),PackageInternalNs(""),Namespace("http://adobe.com/AS3/2006/builtin"),PackageNamespace("flash.text"),PackageNamespace("flash.external"),PackageNamespace("it.gotoandplay.smartfoxserver"),Namespace("com.facebook.graph"),ProtectedNamespace("Game"),StaticProtectedNs("Game"),StaticProtectedNs("flash.display:MovieClip"),StaticProtectedNs("flash.display:Sprite"),StaticProtectedNs("flash.display:DisplayObjectContainer"),StaticProtectedNs("flash.display:InteractiveObject"),StaticProtectedNs("flash.display:DisplayObject"),StaticProtectedNs("flash.events:EventDispatcher")]) + setlocal 5 + pushbyte 49 + getlocal 5 + ifstrictne ofs0084 + pushbyte 0 + jump ofs00a2 + ofs0084: + pushbyte 69 + getlocal 5 + ifstrictne ofs0092 + pushbyte 1 + jump ofs00a2 + ofs0092: + pushbyte 113 + getlocal 5 + ifstrictne ofs00a0 + pushbyte 2 + jump ofs00a2 + ofs00a0: + pushbyte 3 + ofs00a2: + kill 5 + lookupswitch ofs0066, [ofs005a, ofs005c, ofs005e, ofs0066] + ofs00b5: + returnvoid + end ; code + end ; body + end ; method + end ; trait + end ; instance + cinit + refid "tests:TestSwitchMostCommon/class/init" + body + maxstack 1 + localcount 1 + initscopedepth 3 + maxscopedepth 4 + code + getlocal0 + pushscope + + returnvoid + end ; code + end ; body + end ; method +end ; class diff --git a/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/tests/TestSwitchMostCommon.script.asasm b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/tests/TestSwitchMostCommon.script.asasm new file mode 100644 index 000000000..fc6bb12d9 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/as3_assembled/abc/as3_assembled-0/tests/TestSwitchMostCommon.script.asasm @@ -0,0 +1,29 @@ +script + sinit + refid "tests:TestSwitchMostCommon/init" + body + maxstack 2 + localcount 1 + initscopedepth 1 + maxscopedepth 3 + code + getlocal0 + pushscope + + findpropstrict Multiname("TestSwitchMostCommon", [PackageNamespace("tests")]) + getlex QName(PackageNamespace(""), "Object") + pushscope + + getlex Multiname("Object", [PrivateNamespace(null, "tests:TestSwitchMostCommon"), PackageNamespace(""), PackageNamespace("tests"), PackageInternalNs("tests"), Namespace("http://adobe.com/AS3/2006/builtin")]) + newclass "tests:TestSwitchMostCommon" + popscope + initproperty QName(PackageNamespace("tests"), "TestSwitchMostCommon") + + returnvoid + end ; code + end ; body + end ; method + trait class QName(PackageNamespace("tests"), "TestSwitchMostCommon") + #include "TestSwitchMostCommon.class.asasm" + end ; trait +end ; script diff --git a/libsrc/ffdec_lib/testdata/as3_assembled/bin/as3_assembled.swf b/libsrc/ffdec_lib/testdata/as3_assembled/bin/as3_assembled.swf index 72ba76281..c02438b40 100644 Binary files a/libsrc/ffdec_lib/testdata/as3_assembled/bin/as3_assembled.swf and b/libsrc/ffdec_lib/testdata/as3_assembled/bin/as3_assembled.swf differ diff --git a/src/com/jpexs/decompiler/flash/gui/abc/DecompiledEditorPane.java b/src/com/jpexs/decompiler/flash/gui/abc/DecompiledEditorPane.java index 034dddaea..21c385f28 100644 --- a/src/com/jpexs/decompiler/flash/gui/abc/DecompiledEditorPane.java +++ b/src/com/jpexs/decompiler/flash/gui/abc/DecompiledEditorPane.java @@ -47,6 +47,7 @@ import com.jpexs.decompiler.flash.tags.ABCContainerTag; import com.jpexs.decompiler.graph.DottedChain; import com.jpexs.decompiler.graph.TypeItem; import com.jpexs.helpers.CancellableWorker; +import com.jpexs.helpers.Helper; import com.jpexs.helpers.Reference; import java.awt.Point; import java.awt.event.MouseEvent; @@ -881,11 +882,15 @@ public class DecompiledEditorPane extends DebuggableEditorPane implements CaretL protected Void doInBackground() throws Exception { if (decompileNeeded) { + //long timeBefore = System.currentTimeMillis(); View.execInEventDispatch(() -> { setText("// " + AppStrings.translate("work.decompiling") + "..."); }); HighlightedText htext = SWF.getCached(scriptLeaf); + //long timeAfter = System.currentTimeMillis(); + //long delta = timeAfter - timeBefore; + //System.err.println("Finished in " + Helper.formatTimeSec(delta)); View.execInEventDispatch(() -> { setSourceCompleted(scriptLeaf, htext); });