- #2070 - SWF to XML format has new meta fields describing XML export major/minor version
(major = uncompatible change)

### Fixed
- #2070 - Handling newlines and tabs in string values inside SWF to XML export

### Changed
- #2070 - String values inside SWF to XML export are backslash escaped to properly handle newlines and tabs.
Older versions of FFDec can read this new format wrong and corrupt SWFs.
Major version of SWF to XML export changed to 2.
This commit is contained in:
Jindra Petřík
2023-09-05 21:34:25 +02:00
parent 99b697b4ed
commit f4460c6078
5 changed files with 178 additions and 34 deletions

View File

@@ -53,6 +53,9 @@ import javax.xml.stream.XMLStreamWriter;
*/
public class SwfXmlExporter {
public static final int XML_EXPORT_VERSION_MAJOR = 2;
public static final int XML_EXPORT_VERSION_MINOR = 0;
private static final Logger logger = Logger.getLogger(SwfXmlExporter.class.getName());
private final Map<Class, List<Field>> cachedFields = new HashMap<>();
@@ -65,18 +68,17 @@ public class SwfXmlExporter {
XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(writer);
xmlWriter.writeStartDocument();
xmlWriter.writeComment("WARNING: The structure of this XML is not final. In later versions of FFDec it can be changed.");
xmlWriter.writeComment(ApplicationInfo.applicationVerName);
xmlWriter.writeComment("WARNING: The structure of this XML is not final. In later versions of FFDec it can be changed. Make sure you use compatible reader/writer based on _xmlExportMajor/_xmlExportMinor keys.");
exportXml(swf, xmlWriter);
xmlWriter.writeEndDocument();
xmlWriter.flush();
xmlWriter.close();
}
if (!new XmlPrettyFormat().prettyFormat(tmp, outFile, 2, true)) {
logger.log(Level.SEVERE, "Cannot prettyformat SVG");
logger.log(Level.SEVERE, "Cannot prettyformat XML");
}
tmp.delete();
} catch (Exception ex) {
@@ -89,7 +91,7 @@ public class SwfXmlExporter {
}
public void exportXml(SWF swf, XMLStreamWriter writer) throws IOException, XMLStreamException {
generateXml(writer, "swf", swf, false, false);
generateXml(writer, "swf", swf, false);
}
public List<Field> getSwfFieldsCached(Class cls) {
@@ -102,14 +104,14 @@ public class SwfXmlExporter {
});
result.sort((o1, o2) -> {
boolean a1 = canBeAttribute(o1.getType());
boolean a2 = canBeAttribute(o2.getType());
if(a1 == a2 && a1 == true) {
if (a1 == a2 && a1 == true) {
return o1.getName().compareTo(o2.getName());
}
return a1 ? -1 : a2 ? 1 : 0;
});
@@ -142,26 +144,25 @@ public class SwfXmlExporter {
return cls != null && (cls.isArray() || List.class.isAssignableFrom(cls));
}
private void generateXml(XMLStreamWriter writer, String name, Object obj, boolean isListItem, boolean needsCData) throws XMLStreamException {
private void generateXml(XMLStreamWriter writer, String name, Object obj, boolean isListItem) throws XMLStreamException {
Class cls = obj != null ? obj.getClass() : null;
if (obj != null && needsCData && cls == String.class) {
/*if (obj != null && cls == String.class) {
writer.writeStartElement(name);
writer.writeAttribute("type", "String");
writer.writeCData((String) obj);
writer.writeEndElement();
} else if (obj != null && isPrimitive(cls)) {
} else */
if (obj != null && isPrimitive(cls)) {
Object value = obj;
if (value instanceof String) {
value = Helper.removeInvalidXMLCharacters((String) value);
}
String stringValue = Helper.escapeXmlExportString(value.toString());
if (isListItem) {
writer.writeStartElement(name);
writer.writeCharacters(value.toString());
writer.writeCharacters(stringValue);
writer.writeEndElement();
} else {
writer.writeAttribute(name, value.toString());
writer.writeAttribute(name, stringValue);
}
} else if (cls != null && obj != null && cls.isEnum()) {
writer.writeAttribute(name, obj.toString());
@@ -181,7 +182,7 @@ public class SwfXmlExporter {
writer.writeStartElement(name);
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
generateXml(writer, "item", Array.get(value, i), true, false);
generateXml(writer, "item", Array.get(value, i), true);
}
writer.writeEndElement();
} else if (obj != null) {
@@ -197,26 +198,33 @@ public class SwfXmlExporter {
}
writer.writeStartElement(name);
if (obj instanceof SWF) {
writer.writeAttribute("_xmlExportMajor", "" + XML_EXPORT_VERSION_MAJOR);
writer.writeAttribute("_xmlExportMinor", "" + XML_EXPORT_VERSION_MINOR);
writer.writeAttribute("_generator", ApplicationInfo.applicationVerName);
}
writer.writeAttribute("type", clazz.getSimpleName());
if (obj instanceof UnknownTag) {
writer.writeAttribute("tagId", String.valueOf(((Tag) obj).getId()));
}
}
if (obj instanceof SWF) {
writer.writeAttribute("charset", ((SWF) obj).getCharset());
writer.writeAttribute("charset", ((SWF) obj).getCharset());
}
for (Field f : fields) {
Multiline multilineA = f.getAnnotation(Multiline.class);
//Multiline multilineA = f.getAnnotation(Multiline.class);
try {
f.setAccessible(true);
generateXml(writer, f.getName(), f.get(obj), false, multilineA != null);
generateXml(writer, f.getName(), f.get(obj), false);
} catch (IllegalArgumentException | IllegalAccessException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
writer.writeEndElement();
} else if (isListItem) {
writer.writeStartElement(name);

View File

@@ -111,6 +111,7 @@ import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord;
import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.HashArrayList;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.ReflectionTools;
import com.jpexs.helpers.utf8.Utf8InputStreamReader;
import java.io.BufferedInputStream;
@@ -139,7 +140,9 @@ import javax.xml.stream.XMLStreamReader;
*/
@SuppressWarnings("unchecked")
public class SwfXmlImporter {
public static final int MAX_XML_IMPORT_VERSION_MAJOR = 2;
private static final Logger logger = Logger.getLogger(SwfXmlImporter.class.getName());
private static final Map<String, Class> swfTags;
@@ -246,7 +249,7 @@ public class SwfXmlImporter {
XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
try {
XMLStreamReader reader = xmlFactory.createXMLStreamReader(new StringReader(xml));
return processObject(reader, requiredType, swf, null);
return processObject(reader, requiredType, swf, null, 1);
} catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InstantiationException | InvocationTargetException | XMLStreamException ex) {
Logger.getLogger(SwfXmlImporter.class.getName()).log(Level.SEVERE, null, ex);
}
@@ -302,6 +305,23 @@ public class SwfXmlImporter {
String value = reader.getAttributeValue(i);
attributes.put(name, value);
}
int xmlExportMajor = 1;
int xmlExportMinor = 0;
if ("SWF".equals(attributes.get("type")))
{
if (attributes.containsKey("_xmlExportMajor")) {
xmlExportMajor = Integer.parseInt(attributes.get("_xmlExportMajor"));
}
if (attributes.containsKey("_xmlExportMinor")) {
xmlExportMinor = Integer.parseInt(attributes.get("_xmlExportMinor"));
}
if (xmlExportMajor > MAX_XML_IMPORT_VERSION_MAJOR) {
throw new RuntimeException("The XML file was exported with newer XML format (major " + xmlExportMajor+", minor "+xmlExportMinor+"). Please download newer version of FFDec to correctly parse it.");
}
}
for (Map.Entry<String, String> entry : attributes.entrySet()) {
String name = entry.getKey();
@@ -309,12 +329,17 @@ public class SwfXmlImporter {
if (name.equals("tagId") && "UnknownTag".equals(attributes.get("type"))) {
continue;
}
}
if (name.equals("charset") && "SWF".equals(attributes.get("type"))) {
((SWF) obj).setCharset(val);
continue;
}
//skip meta parameters starting with "_". expandable in the future...
if ("SWF".equals(attributes.get("type")) && name.startsWith("_")) {
continue;
}
//backwards compatibility
if (name.equals("reserved1") && "FileAttributesTag".equals(attributes.get("type"))) {
name = "reservedA";
@@ -330,7 +355,7 @@ public class SwfXmlImporter {
if (!name.equals("type")) {
try {
Field field = getField(cls, name);
setFieldValue(field, obj, getAs(field.getType(), val));
setFieldValue(field, obj, getAs(field.getType(), val, xmlExportMajor));
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
logger.log(Level.SEVERE, null, ex);
}
@@ -353,7 +378,7 @@ public class SwfXmlImporter {
// Check for list item elements
reader.nextTag();
while(reader.isStartElement()) {
Object childObj = processObject(reader, reqType, swf, tag);
Object childObj = processObject(reader, reqType, swf, tag, xmlExportMajor);
list.add(childObj);
reader.nextTag();
@@ -371,7 +396,7 @@ public class SwfXmlImporter {
setFieldValue(field, obj, value);
}
else {
Object childObj = processObject(reader, null, swf, tag);
Object childObj = processObject(reader, null, swf, tag, xmlExportMajor);
setFieldValue(field, obj, childObj);
}
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InstantiationException | InvocationTargetException ex) {
@@ -389,7 +414,7 @@ public class SwfXmlImporter {
}
}
private Object processObject(XMLStreamReader reader, Class requiredType, SWF swf, Tag tag) throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, XMLStreamException {
private Object processObject(XMLStreamReader reader, Class requiredType, SWF swf, Tag tag, int xmlExportMajor) throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, XMLStreamException {
// Check if element started and start if needed
if(!reader.isStartElement()) {
reader.nextTag();
@@ -429,7 +454,7 @@ public class SwfXmlImporter {
if (Boolean.parseBoolean(isNullAttr)) {
ret = null;
} else {
ret = getAs(requiredType, reader.getElementText());
ret = getAs(requiredType, reader.getElementText(), xmlExportMajor);
}
}
@@ -473,7 +498,7 @@ public class SwfXmlImporter {
return null;
}
private Object getAs(Class cls, String stringValue) throws IllegalArgumentException, IllegalAccessException {
private Object getAs(Class cls, String stringValue, int xmlExportMajor) throws IllegalArgumentException, IllegalAccessException {
if (cls == Byte.class || cls == byte.class) {
return Byte.parseByte(stringValue);
} else if (cls == Short.class || cls == short.class) {
@@ -491,7 +516,7 @@ public class SwfXmlImporter {
} else if (cls == Character.class || cls == char.class) {
return stringValue.charAt(0);
} else if (cls == String.class) {
return stringValue;
return xmlExportMajor >= 2 ? Helper.unescapeXmlExportString(stringValue) : stringValue;
} else if (cls == ByteArrayRange.class) {
ByteArrayRange range = new ByteArrayRange(stringValue);
return range;

View File

@@ -53,6 +53,7 @@ import com.jpexs.decompiler.flash.types.RGBA;
import com.jpexs.decompiler.flash.types.TEXTRECORD;
import com.jpexs.decompiler.flash.types.annotations.Conditional;
import com.jpexs.decompiler.flash.types.annotations.EnumValue;
import com.jpexs.decompiler.flash.types.annotations.Multiline;
import com.jpexs.decompiler.flash.types.annotations.SWFType;
import com.jpexs.decompiler.flash.types.annotations.SWFVersion;
import com.jpexs.helpers.ByteArrayRange;
@@ -172,6 +173,7 @@ public class DefineEditTextTag extends TextTag {
public String variableName;
@Conditional("hasText")
@Multiline
public String initialText;
public static final int ALIGN_LEFT = 0;

View File

@@ -1244,6 +1244,104 @@ public class Helper {
return false;
}
public static String escapeXmlExportString(String s) {
StringBuilder ret = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\n') {
ret.append("\\n");
} else if (c == '\r') {
ret.append("\\r");
} else if (c == '\t') {
ret.append("\\t");
} else if (c == '\b') {
ret.append("\\b");
} else if (c == '\f') {
ret.append("\\f");
} else if (c == '\\') {
ret.append("\\\\");
} else if (c < 32) {
ret.append("\\u00").append(byteToHex((byte) c));
} else if (!isCharacterValidInXml(c)){
ret.append("\\u").append(String.format("%04x", (int) c));
} else {
ret.append(c);
}
}
return ret.toString();
}
public static String unescapeXmlExportString(String st) {
StringBuilder sb = new StringBuilder(st.length());
for (int i = 0; i < st.length(); i++) {
char ch = st.charAt(i);
if (ch == '\\') {
char nextChar = (i == st.length() - 1) ? '\\' : st
.charAt(i + 1);
// Octal escape?
if (nextChar >= '0' && nextChar <= '7') {
String code = "" + nextChar;
i++;
if ((i < st.length() - 1) && st.charAt(i + 1) >= '0'
&& st.charAt(i + 1) <= '7') {
code += st.charAt(i + 1);
i++;
if ((i < st.length() - 1) && st.charAt(i + 1) >= '0'
&& st.charAt(i + 1) <= '7') {
code += st.charAt(i + 1);
i++;
}
}
sb.append((char) Integer.parseInt(code, 8));
continue;
}
switch (nextChar) {
case '\\':
ch = '\\';
break;
case 'b':
ch = '\b';
break;
case 'f':
ch = '\f';
break;
case 'n':
ch = '\n';
break;
case 'r':
ch = '\r';
break;
case 't':
ch = '\t';
break;
// Hex Unicode: u????
case 'u':
if (i >= st.length() - 5) {
ch = 'u';
break;
}
int code = Integer.parseInt(
"" + st.charAt(i + 2) + st.charAt(i + 3)
+ st.charAt(i + 4) + st.charAt(i + 5), 16);
sb.append(Character.toChars(code));
i += 5;
continue;
}
i++;
}
sb.append(ch);
}
return sb.toString();
}
public static String removeInvalidXMLCharacters(String text) {
StringBuilder sb = new StringBuilder(text.length());