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());