Fixed: #2586 AS3 direct editation + decompilation - XML escape sequences and other XML problems

This commit is contained in:
Jindra Petřík
2026-01-03 23:58:04 +01:00
parent 41b9d1874b
commit b9e00fd673
11 changed files with 255 additions and 14 deletions

View File

@@ -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

Binary file not shown.

View File

@@ -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<Boolean> inAttributeRef, Reference<Boolean> 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("&#13;");
break;
case '\n':
writer.append("&#10;");
break;
case '\t':
writer.append("&#9;");
break;
default:
writer.append(c);
}
} else {
writer.append(c);
}
}
}
return writer.toString();
}
@Override
public GraphTextWriter appendTo(GraphTextWriter writer, LocalData localData) throws InterruptedException {
Reference<Boolean> inAttributeRef = new Reference<>(false);
Reference<Boolean> 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 {

View File

@@ -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<List<NamespaceItem>> allOpenedNamespaces, TypeItem thisType, NamespaceItem pkg, Reference<Boolean> needsActivation, List<DottedChain> importedClasses, List<NamespaceItem> openedNamespaces, Stack<Loop> loops, Map<Loop, String> loopLabels, HashMap<String, Integer> registerVars, boolean inFunction, boolean inMethod, int forinlevel, boolean mustBeCommand, List<AssignableAVM2Item> variables, ABC abc) throws IOException, AVM2ParseException, InterruptedException {
LexBufferer buf = new LexBufferer();

View File

@@ -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 = <elem name=\"aaa\" value=\"xxx&#10;\"/>;\r\n"
+ "var x2:XML = <elem\r\n"
+ "name=\"aaa\"\r\n"
+ "value=\"xxx\"\r\n"
+ "/>;\r\n"
+ "var x3:XML = <elem\r\n"
+ "name=\"aaa\" value=\"xxx\">\r\n"
+ "<sub title=\"yyy\">\r\n"
+ "ampersand: &amp;\r\n"
+ "</sub>\r\n"
+ "<sub />\r\n"
+ "</elem>;\r\n"
+ "var x4:XML = <elem>\r\n"
+ "<elem>\r\n"
+ "A\r\n"
+ "</elem>\r\n"
+ "<elem>\r\n"
+ "B\r\n"
+ "</elem>\r\n"
+ "<elem>\r\n"
+ "<elem>\r\n"
+ "C\r\n"
+ "</elem>\r\n"
+ "</elem>\r\n"
+ "</elem>;\r\n"
+ "var x5:XML = <elem attr=\"abc\\r\\n\\tdef\"></elem>;\r\n"
+ "var x_invalid:XML = new XML(\"<aaa >> invalid \\\"\\n\");\r\n"
+ "var a:int = 5;\r\n"
+ "trace(\"B\");\r\n",
false);
}
}

View File

@@ -2850,4 +2850,34 @@ public class ActionScript3ClassicDecompileTest extends ActionScript3DecompileTes
+ "m = myXML.*;\r\n",
false);
}
@Test
public void testXml2() {
decompileMethod("classic", "testXml2", "var x1:XML = <elem name=\"aaa\" value=\"xxx&#10;\"/>;\r\n"
+ "var x2:XML = <elem name=\"aaa\" value=\"xxx\"/>;\r\n"
+ "var x3:XML = <elem name=\"aaa\" value=\"xxx\">\r\n"
+ "<sub title=\"yyy\">\r\n"
+ "ampersand: &amp;\r\n"
+ "</sub>\r\n"
+ "<sub/>\r\n"
+ "</elem>;\r\n"
+ "var x4:XML = <elem>\r\n"
+ "<elem>\r\n"
+ "A\r\n"
+ "</elem>\r\n"
+ "<elem>\r\n"
+ "B\r\n"
+ "</elem>\r\n"
+ "<elem>\r\n"
+ "<elem>\r\n"
+ "C\r\n"
+ "</elem>\r\n"
+ "</elem>\r\n"
+ "</elem>;\r\n"
+ "var x5:XML = <elem attr=\"abc&#13;&#10;&#9;def\"></elem>;\r\n"
+ "var x_invalid:XML = new XML(\"<aaa >> invalid \\\"\\n\");\r\n"
+ "var a:int = 5;\r\n"
+ "trace(\"B\");\r\n",
false);
}
}

View File

@@ -162,6 +162,7 @@ package
TestWhileTry;
TestWhileTry2;
TestXml;
TestXml2;
SetupMyPackage1;
SetupMyPackage2;

View File

@@ -0,0 +1,43 @@
package tests
{
public class TestXml2
{
public function run():*
{
var x1:XML = new XML("<elem name=\"aaa\" value=\"xxx\n\"/>");
var x2:XML = <elem
name="aaa"
value="xxx"
/>;
var x3:XML = <elem
name="aaa" value="xxx">
<sub title="yyy">
ampersand: &amp;
</sub>
<sub />
</elem>;
var x4:XML = <elem>
<elem>
A
</elem>
<elem>
B
</elem>
<elem>
<elem>
C
</elem>
</elem>
</elem>;
var x5:XML = <elem attr="abc\r\n\tdef"></elem>;
var x_invalid:XML = new XML("<aaa >> invalid \"\n");
var a:int = 5;
trace("B");
}
}
}

View File

@@ -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<Token> 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 = "</" {XMLIdentifier} " "* ">" | "</{" {XMLIdentifier} "}" " "* ">"
XMLAttribute = {WhiteSpace}* {XMLIdentifier} {WhiteSpace}* "=" {WhiteSpace}* (\" {InputCharacter}* \" | "'" {InputCharacter}* "'") {WhiteSpace}*
XMLBeginOneTag = "<" {XMLIdentifier} {XMLAttribute}* {WhiteSpace}* "/"? ">" | "<{" {XMLIdentifier} "}" {XMLAttribute}* {WhiteSpace}* "/"? ">"
XMLBeginTag = "<" {XMLIdentifier} {WhiteSpace}+ | "<{" {XMLIdentifier} "}" {WhiteSpace}
XMLEndTag = "</" {XMLIdentifier} {WhiteSpace}* ">" | "</{" {XMLIdentifier} "}" {WhiteSpace}* ">"
/* 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();}
}
<XML> {
{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);
}