diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4646889..74354dec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ All notable changes to this project will be documented in this file. - [#2572] SVG import - incorrect stroke width when transform contains rotation/shear - [#2595] FLA export - incorrect handling of imported fonts - FLA export - incorrect handling of imported sprites +- [#2586] AS3 direct editation + decompilation - XML escape sequences and other XML problems ### Changed - [#2575] dumpSWF CLI command only allows single SWF dump (no imports, etc.) @@ -4087,6 +4088,7 @@ Major version of SWF to XML export changed to 2. [#1893]: https://www.free-decompiler.com/flash/issues/1893 [#2572]: https://www.free-decompiler.com/flash/issues/2572 [#2595]: https://www.free-decompiler.com/flash/issues/2595 +[#2586]: https://www.free-decompiler.com/flash/issues/2586 [#2556]: https://www.free-decompiler.com/flash/issues/2556 [#2536]: https://www.free-decompiler.com/flash/issues/2536 [#2537]: https://www.free-decompiler.com/flash/issues/2537 diff --git a/lib/jsyntaxpane-0.9.5.jar b/lib/jsyntaxpane-0.9.5.jar index 2b780ced8..202474598 100644 Binary files a/lib/jsyntaxpane-0.9.5.jar and b/lib/jsyntaxpane-0.9.5.jar differ diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/model/XMLAVM2Item.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/model/XMLAVM2Item.java index 121e636b8..5ed50569c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/model/XMLAVM2Item.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/model/XMLAVM2Item.java @@ -16,6 +16,8 @@ */ package com.jpexs.decompiler.flash.abc.avm2.model; +import com.jpexs.decompiler.flash.abc.avm2.parser.script.AbcIndexing; +import com.jpexs.decompiler.flash.abc.avm2.parser.script.ActionScript3Parser; import com.jpexs.decompiler.flash.helpers.GraphTextWriter; import com.jpexs.decompiler.graph.DottedChain; import com.jpexs.decompiler.graph.GraphSourceItem; @@ -23,8 +25,17 @@ import com.jpexs.decompiler.graph.GraphTargetItem; import com.jpexs.decompiler.graph.GraphTargetVisitorInterface; import com.jpexs.decompiler.graph.TypeItem; import com.jpexs.decompiler.graph.model.LocalData; +import com.jpexs.helpers.Helper; +import com.jpexs.helpers.Reference; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * XML. @@ -40,6 +51,7 @@ public class XMLAVM2Item extends AVM2Item { /** * Constructor. + * * @param instruction Instruction * @param lineStartIns Line start instruction * @param parts Parts @@ -54,14 +66,104 @@ public class XMLAVM2Item extends AVM2Item { visitor.visitAll(parts); } + private String handleSingleXml(String s, Reference inAttributeRef, Reference inOpeningTagRef) { + + String identRegexp = "[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][\\-\\.0-9:A-Z_a-z\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0300-\\u036F\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD]*"; + + StringBuilder writer = new StringBuilder(); + + for (int j = 0; j < s.length(); j++) { + char c = s.charAt(j); + switch (c) { + case '"': + if (inOpeningTagRef.getVal()) { + inAttributeRef.setVal(!inAttributeRef.getVal()); + } + writer.append(c); + break; + case '<': + Pattern p = Pattern.compile("^(" + identRegexp + ").*", Pattern.MULTILINE | Pattern.DOTALL); + String sub = s.substring(j + 1); + Matcher m = p.matcher(sub); + writer.append(c); + if (m.matches()) { + inOpeningTagRef.setVal(true); + String tag = m.group(1); + writer.append(tag); + j += tag.length(); + } + break; + case '>': + if (inOpeningTagRef.getVal()) { + inOpeningTagRef.setVal(false); + } + writer.append(c); + break; + default: + if (inAttributeRef.getVal()) { + switch (c) { + case '\r': + writer.append(" "); + break; + case '\n': + writer.append(" "); + break; + case '\t': + writer.append(" "); + break; + default: + writer.append(c); + } + } else { + writer.append(c); + } + } + } + return writer.toString(); + } + @Override public GraphTextWriter appendTo(GraphTextWriter writer, LocalData localData) throws InterruptedException { + + Reference inAttributeRef = new Reference<>(false); + Reference inOpeningTagRef = new Reference<>(false); + + if (parts.size() == 1 && parts.get(0) instanceof StringAVM2Item) { + String s = ((StringAVM2Item) parts.get(0)).getValue(); + s = handleSingleXml(s, inAttributeRef, inOpeningTagRef); + boolean validXml = true; + try { + ActionScript3Parser par = new ActionScript3Parser(new AbcIndexing()); + if (!par.checkBasicXmlOnly(s)) { + validXml = false; + } + } catch (Throwable ex) { + validXml = false; + } + + if (!validXml) { + writer.append("new XML"); + writer.spaceBeforeCallParenthesis(1); + writer.append("(\"").append(Helper.escapeActionScriptString(s)).append("\")"); + return writer; + } + writer.spaceBeforeCallParenthesis(precedence); + writer.append(s); + return writer; + } + for (int i = 0; i < parts.size(); i++) { GraphTargetItem part = parts.get(i); GraphTargetItem partBefore = i > 0 ? parts.get(i - 1) : null; GraphTargetItem partAfter = i < parts.size() - 1 ? parts.get(i + 1) : null; + + /* + Older versions of Flex allow inserting escape sequences like \r, \n, \t + into attributes. Air does not allow this. We handle this by converting it into XML entities. + */ if (part instanceof StringAVM2Item) { String s = ((StringAVM2Item) part).getValue(); + if (partAfter instanceof EscapeXAttrAVM2Item) { if (s.endsWith("\"")) { s = s.substring(0, s.length() - 1); @@ -72,7 +174,8 @@ public class XMLAVM2Item extends AVM2Item { s = s.substring(1); } } - writer.append(s); + + writer.append(handleSingleXml(s, inAttributeRef, inOpeningTagRef)); } else if ((part instanceof EscapeXElemAVM2Item) || (part instanceof EscapeXAttrAVM2Item)) { part.toString(writer, localData); } else { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/parser/script/ActionScript3Parser.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/parser/script/ActionScript3Parser.java index fba189428..f2f86ebf2 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/parser/script/ActionScript3Parser.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/abc/avm2/parser/script/ActionScript3Parser.java @@ -1572,6 +1572,21 @@ public class ActionScript3Parser { //TODO: Order of additions as in official compiler return ret; } + + public boolean checkBasicXmlOnly(String txt) { + lexer = new ActionScriptLexer(txt); + try { + xml(new ArrayList<>(), null, null, new Reference<>(false), new ArrayList<>(), new ArrayList<>(), new HashMap<>(), false, false, new ArrayList<>(), null); + + ParsedSymbol s = lexer.lex(); + if (s.type != SymbolType.EOF) { + return false; + } + } catch (Exception ex) { + return false; + } + return true; + } private GraphTargetItem command(List> allOpenedNamespaces, TypeItem thisType, NamespaceItem pkg, Reference needsActivation, List importedClasses, List openedNamespaces, Stack loops, Map loopLabels, HashMap registerVars, boolean inFunction, boolean inMethod, int forinlevel, boolean mustBeCommand, List variables, ABC abc) throws IOException, AVM2ParseException, InterruptedException { LexBufferer buf = new LexBufferer(); diff --git a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicAirDecompileTest.java b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicAirDecompileTest.java index 802b61597..dfc5eaae5 100644 --- a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicAirDecompileTest.java +++ b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicAirDecompileTest.java @@ -2847,4 +2847,38 @@ public class ActionScript3ClassicAirDecompileTest extends ActionScript3Decompile + "var m:XMLList = myXML.*;\r\n", false); } + + @Test + public void testXml2() { + decompileMethod("classic_air", "testXml2", "var x1:XML = ;\r\n" + + "var x2:XML = ;\r\n" + + "var x3:XML = \r\n" + + "\r\n" + + "ampersand: &\r\n" + + "\r\n" + + "\r\n" + + ";\r\n" + + "var x4:XML = \r\n" + + "\r\n" + + "A\r\n" + + "\r\n" + + "\r\n" + + "B\r\n" + + "\r\n" + + "\r\n" + + "\r\n" + + "C\r\n" + + "\r\n" + + "\r\n" + + ";\r\n" + + "var x5:XML = ;\r\n" + + "var x_invalid:XML = new XML(\"> invalid \\\"\\n\");\r\n" + + "var a:int = 5;\r\n" + + "trace(\"B\");\r\n", + false); + } } diff --git a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicDecompileTest.java b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicDecompileTest.java index 24d5fea38..7740f9231 100644 --- a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicDecompileTest.java +++ b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/as3decompile/ActionScript3ClassicDecompileTest.java @@ -2850,4 +2850,34 @@ public class ActionScript3ClassicDecompileTest extends ActionScript3DecompileTes + "m = myXML.*;\r\n", false); } + + @Test + public void testXml2() { + decompileMethod("classic", "testXml2", "var x1:XML = ;\r\n" + + "var x2:XML = ;\r\n" + + "var x3:XML = \r\n" + + "\r\n" + + "ampersand: &\r\n" + + "\r\n" + + "\r\n" + + ";\r\n" + + "var x4:XML = \r\n" + + "\r\n" + + "A\r\n" + + "\r\n" + + "\r\n" + + "B\r\n" + + "\r\n" + + "\r\n" + + "\r\n" + + "C\r\n" + + "\r\n" + + "\r\n" + + ";\r\n" + + "var x5:XML = ;\r\n" + + "var x_invalid:XML = new XML(\"> invalid \\\"\\n\");\r\n" + + "var a:int = 5;\r\n" + + "trace(\"B\");\r\n", + false); + } } diff --git a/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.air.swf b/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.air.swf index df26b25dd..e4f4252a5 100644 Binary files a/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.air.swf and b/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.air.swf differ diff --git a/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.flex.swf b/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.flex.swf index 5d97f0336..369e9f05a 100644 Binary files a/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.flex.swf and b/libsrc/ffdec_lib/testdata/as3_new/bin/as3_new.flex.swf differ diff --git a/libsrc/ffdec_lib/testdata/as3_new/src/Main.as b/libsrc/ffdec_lib/testdata/as3_new/src/Main.as index 0e32e84dd..71b0045df 100644 --- a/libsrc/ffdec_lib/testdata/as3_new/src/Main.as +++ b/libsrc/ffdec_lib/testdata/as3_new/src/Main.as @@ -162,6 +162,7 @@ package TestWhileTry; TestWhileTry2; TestXml; + TestXml2; SetupMyPackage1; SetupMyPackage2; diff --git a/libsrc/ffdec_lib/testdata/as3_new/src/tests/TestXml2.as b/libsrc/ffdec_lib/testdata/as3_new/src/tests/TestXml2.as new file mode 100644 index 000000000..df887f285 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/as3_new/src/tests/TestXml2.as @@ -0,0 +1,43 @@ +package tests +{ + + public class TestXml2 + { + public function run():* + { + var x1:XML = new XML(""); + var x2:XML = ; + var x3:XML = + + ampersand: & + + + ; + + var x4:XML = + + A + + + B + + + + C + + + ; + + var x5:XML = ; + + var x_invalid:XML = new XML("> invalid \"\n"); + + var a:int = 5; + trace("B"); + } + } +} diff --git a/libsrc/jsyntaxpane/jsyntaxpane/src/main/jflex/jsyntaxpane/lexers/actionscript3.flex b/libsrc/jsyntaxpane/jsyntaxpane/src/main/jflex/jsyntaxpane/lexers/actionscript3.flex index 68c442e7a..d0d1ee08b 100644 --- a/libsrc/jsyntaxpane/jsyntaxpane/src/main/jflex/jsyntaxpane/lexers/actionscript3.flex +++ b/libsrc/jsyntaxpane/jsyntaxpane/src/main/jflex/jsyntaxpane/lexers/actionscript3.flex @@ -58,6 +58,8 @@ import java.util.List; private boolean prevNew = false; + private int xmlNestingLevel = 0; + @Override public void parse(Segment segment, int ofst, List tokens) { try { @@ -115,10 +117,10 @@ IdentifierNs = {Identifier} ":" {Identifier} /* XML */ LetterColon = [:jletter] | ":" XMLIdentifier = {Identifier} | {IdentifierNs} -XMLAttribute = " "* {XMLIdentifier} " "* "=" " "* (\" {InputCharacter}* \" | "'" {InputCharacter}* "'") " "* -XMLBeginOneTag = "<" {XMLIdentifier} {XMLAttribute}* ">" | "<{" {XMLIdentifier} "}" {XMLAttribute}* " "* ">" -XMLBeginTag = "<" {XMLIdentifier} " " | "<{" {XMLIdentifier} "} " -XMLEndTag = "" | "" +XMLAttribute = {WhiteSpace}* {XMLIdentifier} {WhiteSpace}* "=" {WhiteSpace}* (\" {InputCharacter}* \" | "'" {InputCharacter}* "'") {WhiteSpace}* +XMLBeginOneTag = "<" {XMLIdentifier} {XMLAttribute}* {WhiteSpace}* "/"? ">" | "<{" {XMLIdentifier} "}" {XMLAttribute}* {WhiteSpace}* "/"? ">" +XMLBeginTag = "<" {XMLIdentifier} {WhiteSpace}+ | "<{" {XMLIdentifier} "}" {WhiteSpace} +XMLEndTag = "" | "" /* integer literals */ DecIntegerLiteral = (0 | -?[1-9][0-9]*) [ui]? @@ -346,15 +348,20 @@ VerbatimString = "@\"" {VerbatimStringCharacter}* "\"" yypushback(yytext().length() - 1); return token(TokenType.OPERATOR, yychar, 1); } - yybegin(XML); tokenStart = yychar; tokenLength = yylength(); String s=yytext(); - s=s.substring(1,s.length()-1); + s = s.substring(1,s.length()-1); + if (s.endsWith("/")) { + return token(TokenType.STRING, tokenStart, tokenLength); + } + if(s.contains(" ")){ s=s.substring(0,s.indexOf(" ")); } - xmlTagName = s; + yybegin(XML); + xmlTagName = s; + xmlNestingLevel = 1; } ">" { prevNew = false; return token(TokenType.OPERATOR); } @@ -373,12 +380,18 @@ VerbatimString = "@\"" {VerbatimStringCharacter}* "\"" ">" { yybegin(XML); tokenLength += yylength();} } { - {XMLBeginOneTag} { tokenLength += yylength();} - {XMLEndTag} { tokenLength += yylength(); - String endtagname=yytext(); - endtagname = endtagname.substring(2,endtagname.length()-1); - endtagname = endtagname.trim(); - if(endtagname.equals(xmlTagName)){ + {XMLBeginOneTag} { + tokenLength += yylength(); + String s = yytext(); + if (s.charAt(s.length() - 2) != '/') { + xmlNestingLevel++; + } + } + {XMLEndTag} { + xmlNestingLevel--; + + tokenLength += yylength(); + if (xmlNestingLevel == 0) { yybegin(YYINITIAL); return token(TokenType.STRING, tokenStart, tokenLength); }