From f4460c607850320d217825a0ffd6a3bce6de1548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Tue, 5 Sep 2023 21:34:25 +0200 Subject: [PATCH] Added - #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. --- CHANGELOG.md | 11 +++ .../flash/exporters/swf/SwfXmlExporter.java | 56 ++++++----- .../flash/importers/SwfXmlImporter.java | 45 +++++++-- .../flash/tags/DefineEditTextTag.java | 2 + .../src/com/jpexs/helpers/Helper.java | 98 +++++++++++++++++++ 5 files changed, 178 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0cb8025..7a0f893a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added - [#1449] Updated Turkish translation +- [#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. ### Fixed - [#2043] StartSound2 tag handling @@ -3047,6 +3057,7 @@ All notable changes to this project will be documented in this file. [alpha 8]: https://github.com/jindrapetrik/jpexs-decompiler/compare/alpha7...alpha8 [alpha 7]: https://github.com/jindrapetrik/jpexs-decompiler/releases/tag/alpha7 [#1449]: https://www.free-decompiler.com/flash/issues/1449 +[#2070]: https://www.free-decompiler.com/flash/issues/2070 [#2043]: https://www.free-decompiler.com/flash/issues/2043 [#1998]: https://www.free-decompiler.com/flash/issues/1998 [#2038]: https://www.free-decompiler.com/flash/issues/2038 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java index a7a09657e..e212ea31c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java @@ -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> 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 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); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java index d619ae079..2292155a5 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java @@ -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 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 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; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java index 542340d65..29712a39a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java @@ -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; diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java index 3b85ea6e3..1e322a363 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java @@ -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());