From 3712c3be45c4ebf660020e751288c46addcd9fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Tue, 5 Nov 2024 22:21:35 +0100 Subject: [PATCH] Added: #2360 SOL file (Flash Local Shared Object - flash cookie) editor --- CHANGELOG.md | 2 + .../flash/amf/amf0/Amf0InputStream.java | 477 ++++++++++++++ .../flash/amf/amf0/Amf0OutputStream.java | 252 ++++++++ .../decompiler/flash/amf/amf0/Marker.java | 113 ++++ .../flash/amf/amf0/package-info.java | 4 + .../flash/amf/amf0/types/ArrayType.java | 34 + .../flash/amf/amf0/types/BasicType.java | 65 ++ .../flash/amf/amf0/types/ComplexObject.java | 32 + .../flash/amf/amf0/types/DateType.java | 82 +++ .../flash/amf/amf0/types/EcmaArrayType.java | 40 ++ .../flash/amf/amf0/types/ObjectType.java | 40 ++ .../flash/amf/amf0/types/ReferenceType.java | 30 + .../flash/amf/amf0/types/TypedObjectType.java | 41 ++ .../flash/amf/amf0/types/XmlDocumentType.java | 60 ++ .../flash/amf/amf0/types/package-info.java | 4 + .../flash/amf/amf3/Amf3InputStream.java | 18 +- .../flash/amf/amf3/Amf3OutputStream.java | 14 +- .../decompiler/flash/amf/amf3/ListMap.java | 11 +- .../decompiler/flash/amf/amf3/ListSet.java | 125 ---- .../decompiler/flash/amf/amf3/Traits.java | 37 +- .../flash/amf/amf3/types/DictionaryType.java | 3 +- .../flash/amf/amf3/types/ObjectType.java | 12 +- .../flash/configuration/Configuration.java | 4 + .../exporters/amf/amf0/Amf0Exporter.java | 250 ++++++++ .../exporters/amf/amf0/package-info.java | 4 + .../exporters/amf/amf3/Amf3Exporter.java | 26 +- .../importers/amf/amf0/Amf0Importer.java | 583 ++++++++++++++++++ .../importers/amf/amf0/package-info.java | 4 + .../importers/amf/amf3/Amf3Importer.java | 47 +- .../decompiler/flash/sol/FilePathTag.java | 54 ++ .../jpexs/decompiler/flash/sol/LsoTag.java | 149 +++++ .../jpexs/decompiler/flash/sol/SolFile.java | 225 +++++++ .../com/jpexs/decompiler/flash/sol/Tag.java | 50 ++ .../decompiler/flash/sol/UnknownTag.java | 41 ++ .../decompiler/flash/sol/package-info.java | 4 + .../com/jpexs/decompiler/flash/tags/Tag.java | 3 +- .../ffdec_lib/testdata/sharedobjects/Main.as | 13 + .../testdata/sharedobjects/MyClass.as | 12 + .../testdata/sharedobjects/data/amf0test.sol | Bin 0 -> 70385 bytes .../testdata/sharedobjects/data/amf3test.sol | Bin 0 -> 70270 bytes .../testdata/sharedobjects/data/bad.sol | 1 + .../testdata/sharedobjects/sharedobjects.html | 49 ++ .../testdata/sharedobjects/sharedobjects.swf | Bin 0 -> 21507 bytes .../sharedobjects/DOMDocument.xml | 154 +++++ .../sharedobjects/LIBRARY/LoadButton.xml | 102 +++ .../sharedobjects/LIBRARY/SaveButton.xml | 102 +++ .../sharedobjects/META-INF/metadata.xml | 55 ++ .../sharedobjects/MobileSettings.xml | 0 .../sharedobjects/PublishSettings.xml | 206 +++++++ .../sharedobjects/bin/SymDepend.cache | Bin 0 -> 64 bytes .../sharedobjects/sharedobjects.xfl | 1 + src/com/jpexs/decompiler/flash/gui/Main.java | 6 + .../decompiler/flash/gui/MainFrameMenu.java | 7 +- .../flash/gui/graphics/soleditor16.png | Bin 0 -> 1138 bytes .../flash/gui/graphics/soleditor32.png | Bin 0 -> 9107 bytes .../locales/AdvancedSettingsDialog.properties | 6 +- .../flash/gui/locales/MainFrame.properties | 5 +- .../soleditor/SolEditorFrame.properties | 20 + .../flash/gui/soleditor/SolEditorFrame.java | 313 ++++++++++ 59 files changed, 3842 insertions(+), 150 deletions(-) create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0InputStream.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0OutputStream.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Marker.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/package-info.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ArrayType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/BasicType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ComplexObject.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/DateType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/EcmaArrayType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ObjectType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ReferenceType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/TypedObjectType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/XmlDocumentType.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/package-info.java delete mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListSet.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/Amf0Exporter.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/package-info.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/Amf0Importer.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/package-info.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/FilePathTag.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/LsoTag.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/SolFile.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/Tag.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/UnknownTag.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/package-info.java create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/Main.as create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/MyClass.as create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/data/amf0test.sol create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/data/amf3test.sol create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/data/bad.sol create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.html create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.swf create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/DOMDocument.xml create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/LoadButton.xml create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/SaveButton.xml create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/META-INF/metadata.xml create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/MobileSettings.xml create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/PublishSettings.xml create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/bin/SymDepend.cache create mode 100644 libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/sharedobjects.xfl create mode 100644 src/com/jpexs/decompiler/flash/gui/graphics/soleditor16.png create mode 100644 src/com/jpexs/decompiler/flash/gui/graphics/soleditor32.png create mode 100644 src/com/jpexs/decompiler/flash/gui/locales/soleditor/SolEditorFrame.properties create mode 100644 src/com/jpexs/decompiler/flash/gui/soleditor/SolEditorFrame.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f92dcea7..38d9dc8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. Undo / redo feature. Double click movie clips to edit sub-objects. - [#1619] Option to set thread count to 0 for auto setting processor count - 1 +- [#2360] SOL file (Flash Local Shared Object - flash cookie) editor ### Fixed - [#2357] AS3 instance var/const initialization @@ -3650,6 +3651,7 @@ Major version of SWF to XML export changed to 2. [alpha 8]: https://github.com/jindrapetrik/jpexs-decompiler/compare/alpha7...alpha8 [alpha 7]: https://github.com/jindrapetrik/jpexs-decompiler/releases/tag/alpha7 [#1619]: https://www.free-decompiler.com/flash/issues/1619 +[#2360]: https://www.free-decompiler.com/flash/issues/2360 [#2357]: https://www.free-decompiler.com/flash/issues/2357 [#2361]: https://www.free-decompiler.com/flash/issues/2361 [#2362]: https://www.free-decompiler.com/flash/issues/2362 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0InputStream.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0InputStream.java new file mode 100644 index 000000000..9b8885009 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0InputStream.java @@ -0,0 +1,477 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0; + +import com.jpexs.decompiler.flash.EndOfStreamException; +import com.jpexs.decompiler.flash.amf.amf0.types.ArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.BasicType; +import com.jpexs.decompiler.flash.amf.amf0.types.ComplexObject; +import com.jpexs.decompiler.flash.amf.amf0.types.DateType; +import com.jpexs.decompiler.flash.amf.amf0.types.EcmaArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.ObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.ReferenceType; +import com.jpexs.decompiler.flash.amf.amf0.types.TypedObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.XmlDocumentType; +import com.jpexs.decompiler.flash.amf.amf3.Amf3InputStream; +import com.jpexs.decompiler.flash.amf.amf3.NoSerializerExistsException; +import com.jpexs.decompiler.flash.dumpview.DumpInfo; +import com.jpexs.decompiler.flash.ecma.EcmaScript; +import com.jpexs.helpers.Helper; +import com.jpexs.helpers.MemoryInputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * InputStream for AMF0 data. + * + * @author JPEXS + */ +public class Amf0InputStream extends InputStream { + + private final MemoryInputStream is; + + /** + * Dump info + */ + public DumpInfo dumpInfo; + + public Amf0InputStream(MemoryInputStream is) { + this.is = is; + } + + private int readInternal() throws IOException { + int ret = read(); + if (ret == -1) { + throw new EndOfStreamException(); + } + return ret; + } + + public byte[] readBytes(int count) throws IOException { + DataInputStream dais = new DataInputStream(is); + byte[] ret = new byte[count]; + try { + dais.readFully(ret); + } catch (EOFException e) { + throw new EndOfStreamException(); + } + return ret; + } + + @Override + public int read() throws IOException { + return is.read(); + } + + @Override + public int available() throws IOException { + return is.available(); + } + + /** + * New dump level. + * + * @param name Name + * @param type Type + * @return Dump info + */ + public DumpInfo newDumpLevel(String name, String type) { + if (dumpInfo != null) { + long startByte = is.getPos(); + DumpInfo di = new DumpInfo(name, type, null, startByte, 0, 0, 0); + di.parent = dumpInfo; + dumpInfo.getChildInfos().add(di); + dumpInfo = di; + } + + return dumpInfo; + } + + /** + * Ends dump level + */ + public void endDumpLevel() { + endDumpLevel(null); + } + + /** + * Ends dump level + * + * @param value Value + */ + public void endDumpLevel(Object value) { + if (dumpInfo != null) { + dumpInfo.lengthBytes = is.getPos() - dumpInfo.startByte; + dumpInfo.previewValue = value; + dumpInfo = dumpInfo.parent; + } + } + + /** + * Ends dump level until + * + * @param di Dump info + */ + public void endDumpLevelUntil(DumpInfo di) { + if (di != null) { + while (dumpInfo != null && dumpInfo != di) { + endDumpLevel(); + } + } + } + + /** + * Reads U8 (unsigned 8-bit integer) value. + * + * @param name Name + * @return U8 value + * @throws IOException On I/O error + */ + public int readU8(String name) throws IOException { + newDumpLevel(name, "U8"); + int ret = readInternal(); + endDumpLevel(ret); + return ret; + } + + /** + * Reads U16 (unsigned 16-bit integer) value. + * + * @param name Name + * @return U16 value + * @throws IOException On I/O error + */ + public int readU16(String name) throws IOException { + newDumpLevel(name, "U16"); + int b1 = readInternal(); + int b2 = readInternal(); + int ret = (b1 << 8) + b2; + endDumpLevel(ret); + return ret; + } + + /** + * Reads S16 (signed 16-bit integer) value. + * + * @param name Name + * @return S16 value + * @throws IOException On I/O error + */ + public int readS16(String name) throws IOException { + newDumpLevel(name, "S16"); + int b1 = readInternal(); + int b2 = readInternal(); + int ret = (b1 << 8) + b2; + ret = (int) signExtend(ret, 16); + endDumpLevel(ret); + return ret; + } + + /** + * Reads U32 (unsigned 32-bit integer) value. + * + * @param name Name + * @return U32 value + * @throws IOException On I/O error + */ + public long readU32(String name) throws IOException { + newDumpLevel(name, "U32"); + long ret = readU32Internal(); + endDumpLevel(ret); + return ret; + } + + /** + * Reads U32 (unsigned 32-bit integer) value. + * + * @return U32 value + * @throws IOException On I/O error + */ + private long readU32Internal() throws IOException { + int b1 = readInternal(); + int b2 = readInternal(); + int b3 = readInternal(); + int b4 = readInternal(); + + return ((b1 << 24) + (b2 << 16) + (b3 << 8) + b4) & 0xffffffff; + } + + /** + * Reads S32 (signed 32-bit integer) value. + * + * @param name Name + * @return S32 value + * @throws IOException On I/O error + */ + public long readS32(String name) throws IOException { + newDumpLevel(name, "S32"); + long ret = signExtend(readU32Internal(), 32); + endDumpLevel(ret); + return ret; + } + + /** + * Reads long value. + * + * @return Long value + * @throws IOException On I/O error + */ + private long readLong() throws IOException { + byte[] readBuffer = new byte[8]; + for (int i = 0; i < 8; i++) { + readBuffer[i] = (byte) readInternal(); + } + return (((long) readBuffer[0] << 56) + + ((long) (readBuffer[1] & 0xff) << 48) + + ((long) (readBuffer[2] & 0xff) << 40) + + ((long) (readBuffer[3] & 0xff) << 32) + + ((long) (readBuffer[4] & 0xff) << 24) + + ((readBuffer[5] & 0xff) << 16) + + ((readBuffer[6] & 0xff) << 8) + + ((readBuffer[7] & 0xff))); + } + + /** + * Reads double value. + * + * @param name Name + * @return Double value + * @throws IOException On I/O error + */ + public double readDouble(String name) throws IOException { + newDumpLevel(name, "DOUBLE"); + long lval = readLong(); + double ret = Double.longBitsToDouble(lval); + endDumpLevel(EcmaScript.toString(ret)); + return ret; + } + + private long signExtend(long val, int size) { + if (((val >> (size - 1)) & 1) == 1) { //has sign bit + long mask = size == 32 ? 0xFFFFFFFF : (1 << size) - 1; // 111111...up to size + long positiveVal = (~(val - 1)) & mask; + long negativeVal = -positiveVal; + return negativeVal; + } + return val; + } + + /** + * Reads UTF-8 value + * @param name Name + * @return UTF-8 value + * @throws IOException On I/O error + */ + public String readUtf8(String name) throws IOException { + newDumpLevel(name, "UTF-8"); + int len = readU16("length"); + byte[] data = len == 0 ? null : readBytes(len); + String retString = data == null ? "" : new String(data, "UTF-8"); + endDumpLevel("\"" + Helper.escapeActionScriptString(retString) + "\""); + return retString; + } + + /** + * Reads UTF-8-long value + * @param name Name + * @return UTF-8-long value + * @throws IOException On I/O error + */ + public String readUtf8Long(String name) throws IOException { + newDumpLevel(name, "UTF-8-long"); + int len = (int) readU32("length"); //TODO: handle lengths that not fit int + + byte[] data = len == 0 ? null : readBytes(len); + String retString = data == null ? "" : new String(data, "UTF-8"); + endDumpLevel(); + return retString; + } + + public Object readValueWithReferences(String name) throws IOException, NoSerializerExistsException { + Object value = readValue(name); + List complexObjects = new ArrayList<>(); + populateComplexObjects(value, complexObjects); + return resolveReferences(value, complexObjects); + } + + /** + * Reads AMF0 value + * @param name Name + * @return AMF0 value + * @throws IOException On I/O error + * @throws NoSerializerExistsException When reading is switched to AMF3 and no serializer found for an object + */ + public Object readValue(String name) throws IOException, NoSerializerExistsException { + newDumpLevel(name, "value-type"); + try { + int marker = readInternal(); + switch (marker) { + case Marker.NUMBER: + return readDouble("DOUBLE"); + case Marker.BOOLEAN: + return readU8("U8") > 0; + case Marker.STRING: + return readUtf8("UTF-8"); + case Marker.OBJECT_END: + return BasicType.OBJECT_END; + case Marker.OBJECT: + ObjectType object = new ObjectType(); + String propName; + Object val; + + while (true) { + propName = readUtf8("propertyName"); + val = readValue("propertyValue"); + if (propName.equals("")) { + break; + } + object.properties.put(propName, val); + } + return object; + case Marker.MOVIECLIP: + throw new IllegalArgumentException("MovieClip not supported in AMF0"); + case Marker.NULL: + return BasicType.NULL; + case Marker.UNDEFINED: + return BasicType.UNDEFINED; + case Marker.REFERENCE: + return new ReferenceType(readU16("referenceIndex")); + case Marker.ECMA_ARRAY: + int associativeCount = (int) readU32("associative-count"); + EcmaArrayType ea = new EcmaArrayType(); + for (int a = 0; a < associativeCount; a++) { + String eaKey = readUtf8("key"); + Object eaVal = readValue("value"); + ea.values.put(eaKey, eaVal); + } + readUtf8("UTF-8-empty"); + readValue("object-end"); + + return ea; + case Marker.STRICT_ARRAY: + int arrayCount = (int) readU32("array-count"); + ArrayType at = new ArrayType(); + for (int a = 0; a < arrayCount; a++) { + at.values.add(readValue("value")); + } + return at; + case Marker.DATE: + double dval = readDouble("epoch-millis"); + int timezone = readS16("time-zone"); + return new DateType(dval, timezone); + case Marker.LONG_STRING: + return readUtf8Long("long-string"); + case Marker.UNSUPPORTED: + throw new IllegalArgumentException("Unsupported type"); + case Marker.RECORDSET: + throw new IllegalArgumentException("RecordSet not supported in AMF0"); + case Marker.XML_DOCUMENT: + return new XmlDocumentType(readUtf8Long("xml")); + case Marker.TYPED_OBJECT: + String className = readUtf8("class-name"); + TypedObjectType typedObject = new TypedObjectType(); + typedObject.className = className; + + while (true) { + propName = readUtf8("propertyName"); + val = readValue("propertyValue"); + if (propName.equals("")) { + break; + } + typedObject.properties.put(propName, val); + } + return typedObject; + case Marker.AVMPLUS_OBJECT: + Amf3InputStream amf3 = new Amf3InputStream(is); + return amf3.readValue("avm-plus-object"); + default: + throw new IllegalArgumentException("Unsupported type"); + } + } finally { + endDumpLevel(); + } + } + + public void resolveMapReferences(Map map) { + List complexObjects = new ArrayList<>(); + populateComplexObjects((List)new ArrayList<>(map.values()), complexObjects); + + for (String key : map.keySet()) { + map.put(key, resolveReferences(map.get(key), complexObjects)); + } + } + + public Object resolveReferences(Object value, List complexObjects) { + if (value instanceof ReferenceType) { + ReferenceType rt = (ReferenceType) value; + return complexObjects.get(rt.referenceIndex); + } + if (value instanceof ObjectType) { + ObjectType ot = (ObjectType) value; + for (String key : ot.properties.keySet()) { + ot.properties.put(key, resolveReferences(ot.properties.get(key), complexObjects)); + } + } + if (value instanceof TypedObjectType) { + TypedObjectType tot = (TypedObjectType) value; + for (String key : tot.properties.keySet()) { + tot.properties.put(key, resolveReferences(tot.properties.get(key), complexObjects)); + } + } + if (value instanceof EcmaArrayType) { + EcmaArrayType eat = (EcmaArrayType) value; + for (String key : eat.values.keySet()) { + eat.values.put(key, resolveReferences(eat.values.get(key), complexObjects)); + } + } + if (value instanceof ArrayType) { + ArrayType at = (ArrayType) value; + for (int i = 0; i < at.values.size(); i++) { + at.values.set(i, resolveReferences(at.values.get(i), complexObjects)); + } + } + return value; + } + + public void populateComplexObjects(List values, List result) { + for (Object value : values) { + populateComplexObjects(value, result); + } + } + + public void populateComplexObjects(Object value, List result) { + if (result.contains(value)) { + return; + } + if (value instanceof ComplexObject) { + result.add(value); + for (Object subvalue : ((ComplexObject) value).getSubValues()) { + populateComplexObjects(subvalue, result); + } + } + } + + @Override + public long skip(long n) throws IOException { + return is.skip(n); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0OutputStream.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0OutputStream.java new file mode 100644 index 000000000..7ba27e9b0 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Amf0OutputStream.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0; + +import com.jpexs.decompiler.flash.amf.amf0.types.ArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.BasicType; +import com.jpexs.decompiler.flash.amf.amf0.types.DateType; +import com.jpexs.decompiler.flash.amf.amf0.types.EcmaArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.ObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.TypedObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.XmlDocumentType; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * + * @author JPEXS + */ +public class Amf0OutputStream extends OutputStream { + + private final OutputStream os; + /** + * Constructor. + * + * @param os Output stream + */ + public Amf0OutputStream(OutputStream os) { + this.os = os; + } + + /** + * Writes U8 (unsigned 8-bit integer). + * @param v Value + * @throws IOException On I/O error + */ + public void writeU8(int v) throws IOException { + write(v); + } + + /** + * Writes U16 (unsigned 16-bit integer). + * @param v Value + * @throws IOException On I/O error + */ + public void writeU16(int v) throws IOException { + int b1 = (v >> 8) & 0xff; + int b2 = v & 0xff; + write(b1); + write(b2); + } + + /** + * Writes S16 (signed 16-bit integer). + * @param v Value + * @throws IOException On I/O error + */ + public void writeS16(int v) throws IOException { + int b1 = (v >> 8) & 0xff; + int b2 = v & 0xff; + write(b1); + write(b2); + } + + /** + * Writes U32 (unsigned 32-bit integer). + * @param v Value + * @throws IOException On I/O error + */ + public void writeU32(long v) throws IOException { + int b1 = (int) ((v >> 24) & 0xff); + int b2 = (int) ((v >> 16) & 0xff); + int b3 = (int) ((v >> 8) & 0xff); + int b4 = (int) (v & 0xff); + + write(b1); + write(b2); + write(b3); + write(b4); + } + + /** + * Writes double. + * @param v Value + * @throws IOException On I/O error + */ + public void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + /** + * Writes long. + * @param value Value + * @throws IOException On I/O error + */ + private void writeLong(long value) throws IOException { + byte[] writeBuffer = new byte[8]; + writeBuffer[0] = (byte) (value >>> 56); + writeBuffer[1] = (byte) (value >>> 48); + writeBuffer[2] = (byte) (value >>> 40); + writeBuffer[3] = (byte) (value >>> 32); + writeBuffer[4] = (byte) (value >>> 24); + writeBuffer[5] = (byte) (value >>> 16); + writeBuffer[6] = (byte) (value >>> 8); + writeBuffer[7] = (byte) (value); + write(writeBuffer); + } + + /** + * Writes bytes. + * @param data Data + * @throws IOException On I/O error + */ + public void writeBytes(byte[] data) throws IOException { + os.write(data); + } + + @Override + public void write(int v) throws IOException { + os.write(v); + } + + @Override + public void write(byte[] b) throws IOException { + os.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + os.write(b, off, len); + } + + public void writeUtf8(String value) throws IOException { + byte[] data = value.getBytes("UTF-8"); + writeU16(data.length); + writeBytes(data); + } + + public void writeUtf8Long(String value) throws IOException { + byte[] data = value.getBytes("UTF-8"); + writeU32(data.length); + writeBytes(data); + } + + public void writeObjectProperty(String name, Object value, List complexObjectsList) throws IOException { + writeUtf8(name); + writeValue(value, complexObjectsList); + } + + public void writeUtf8Empty() throws IOException { + writeU16(0); + } + + public void writeValue(Object value, List complexObjectsList) throws IOException { + + if ((value instanceof ObjectType) + || (value instanceof TypedObjectType) + || (value instanceof ArrayType) + || (value instanceof EcmaArrayType) + ) { + int index = complexObjectsList.indexOf(value); + if (index != -1 && index <= 65535 ) { + write(Marker.REFERENCE); + writeU16(index); + return; + } else { + complexObjectsList.add(value); + } + } + + if (value instanceof Double) { + write(Marker.NUMBER); + writeDouble((Double) value); + } else if (value instanceof Boolean) { + write(Marker.BOOLEAN); + write(((Boolean) value) ? 1 : 0); + } else if (value instanceof String) { + String sval = (String) value; + if (sval.length() > 65535) { + write(Marker.LONG_STRING); + writeUtf8Long(sval); + } else { + write(Marker.STRING); + writeUtf8(sval); + } + } else if (value instanceof ObjectType) { + write(Marker.OBJECT); + ObjectType ot = (ObjectType) value; + for (String key : ot.properties.keySet()) { + writeObjectProperty(key, ot.properties.get(key), complexObjectsList); + } + writeUtf8Empty(); + write(Marker.OBJECT_END); + } else if (value == BasicType.NULL) { + write(Marker.NULL); + } else if (value == BasicType.UNDEFINED) { + write(Marker.UNDEFINED); + } else if (value instanceof EcmaArrayType) { + write(Marker.ECMA_ARRAY); + EcmaArrayType ea = (EcmaArrayType) value; + writeU32(ea.values.size()); + for (String key : ea.values.keySet()) { + writeObjectProperty(key, ea.values.get(key), complexObjectsList); + } + writeUtf8Empty(); + write(Marker.OBJECT_END); + } else if (value instanceof ArrayType) { + write(Marker.STRICT_ARRAY); + ArrayType at = (ArrayType) value; + writeU32(at.values.size()); + for (Object v : at.values) { + writeValue(v, complexObjectsList); + } + } else if (value instanceof DateType) { + write(Marker.DATE); + DateType dt = (DateType) value; + writeDouble(dt.getVal()); + writeS16(dt.getTimezone()); + } else if (value instanceof XmlDocumentType) { + write(Marker.XML_DOCUMENT); + XmlDocumentType xmlDoc = (XmlDocumentType) value; + writeUtf8Long(xmlDoc.getData()); + } else if (value instanceof TypedObjectType) { + write(Marker.TYPED_OBJECT); + TypedObjectType tot = (TypedObjectType) value; + writeUtf8(tot.className); + for (String key : tot.properties.keySet()) { + writeObjectProperty(key, tot.properties.get(key), complexObjectsList); + } + writeUtf8Empty(); + write(Marker.OBJECT_END); + } else { + throw new IllegalArgumentException("Unsupported value type for serialization"); + } + + //TODO: Switching to AMF3 when necessary + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Marker.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Marker.java new file mode 100644 index 000000000..0c9fb4d23 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/Marker.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0; + +/** + * AMF0 marker. + * @author JPEXS + */ +public class Marker { + /** + * Number + */ + public static final int NUMBER = 0x00; + + /** + * Boolean + */ + public static final int BOOLEAN = 0x01; + + /** + * String + */ + public static final int STRING = 0x02; + + /** + * Object + */ + public static final int OBJECT = 0x03; + + /** + * MovieClip - Reserved, not supported + */ + public static final int MOVIECLIP = 0x04; + + /** + * Null + */ + public static final int NULL = 0x05; + + /** + * Undefined + */ + public static final int UNDEFINED = 0x06; + + /** + * Reference + */ + public static final int REFERENCE = 0x07; + + /** + * Ecma array + */ + public static final int ECMA_ARRAY = 0x08; + + /** + * Object end + */ + public static final int OBJECT_END = 0x09; + + /** + * Strict array + */ + public static final int STRICT_ARRAY = 0x0A; + + /** + * Date + */ + public static final int DATE = 0x0B; + + /** + * Long string + */ + public static final int LONG_STRING = 0x0C; + + /** + * Unsupported + */ + public static final int UNSUPPORTED = 0x0D; + + /** + * Record set - Reserved, not supported + */ + public static final int RECORDSET = 0x0E; + + /** + * XML document + */ + public static final int XML_DOCUMENT = 0x0F; + + /** + * Typed object + */ + public static final int TYPED_OBJECT = 0x10; + + /** + * AvmPlus object + */ + public static final int AVMPLUS_OBJECT = 0x11; +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/package-info.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/package-info.java new file mode 100644 index 000000000..65d417f11 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/package-info.java @@ -0,0 +1,4 @@ +/** + * AMF0. + */ +package com.jpexs.decompiler.flash.amf.amf0; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ArrayType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ArrayType.java new file mode 100644 index 000000000..9ed1dd717 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ArrayType.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import java.util.ArrayList; +import java.util.List; + +/** + * Array type + * @author JPEXS + */ +public class ArrayType implements ComplexObject { + public List values = new ArrayList<>(); + + @Override + public List getSubValues() { + return values; + } + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/BasicType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/BasicType.java new file mode 100644 index 000000000..db9c44d99 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/BasicType.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import com.jpexs.decompiler.flash.amf.amf3.types.*; + +/** + * Basic AMF0 types. + */ +public enum BasicType implements Amf3ValueType { + /** + * Null + */ + NULL { + @Override + public String toString() { + return "null"; + } + + }, + /** + * Undefined + */ + UNDEFINED { + @Override + public String toString() { + return "undefined"; + } + + }, + /** + * Unknown - Special type for errors while reading + */ + UNKNOWN { + @Override + public String toString() { + return "unknown"; + } + + }, + /** + * Object end + */ + OBJECT_END { + @Override + public String toString() { + return "object-end"; + } + + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ComplexObject.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ComplexObject.java new file mode 100644 index 000000000..839627548 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ComplexObject.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import java.util.List; + +/** + * Interface for AMF0 objects that have sub values + */ +public interface ComplexObject { + + /** + * Gets sub values. + * + * @return List of sub values + */ + public List getSubValues(); +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/DateType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/DateType.java new file mode 100644 index 000000000..66b92ddac --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/DateType.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import com.jpexs.decompiler.flash.amf.amf3.types.*; +import com.jpexs.decompiler.flash.exporters.amf.amf3.Amf3Exporter; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * AMF0 date type. + */ +public class DateType implements Amf3ValueType { + + private int timezone; + + private double val; + + /** + * Constructor. + * @param val Date value + * @param timezone Time zone + */ + public DateType(double val, int timezone) { + this.val = val; + this.timezone = timezone; + } + + /** + * Gets date value. + * @return Date value + */ + public double getVal() { + return val; + } + + /** + * Sets date value. + * @param val Date value + */ + public void setVal(double val) { + this.val = val; + } + + /** + * Converts this to date. + * @return Date + */ + public Date toDate() { + return new Date((long) val); + } + + /** + * Gets timezone + * @return Timezone + */ + public int getTimezone() { + return timezone; + } + + @Override + public String toString() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS"); + return sdf.format(toDate()) + " timezone " + timezone; + } + + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/EcmaArrayType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/EcmaArrayType.java new file mode 100644 index 000000000..a5d71f4d7 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/EcmaArrayType.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Ecma Array + * @author JPEXS + */ +public class EcmaArrayType implements ComplexObject { + public Map values = new LinkedHashMap<>(); + + @Override + public String toString() { + return "EcmaArrayType"; + } + + @Override + public List getSubValues() { + return new ArrayList<>(values.values()); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ObjectType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ObjectType.java new file mode 100644 index 000000000..396fe55b2 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ObjectType.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Object type. + * @author JPEXS + */ +public class ObjectType implements ComplexObject { + public Map properties = new LinkedHashMap<>(); + + @Override + public String toString() { + return "ObjectType"; + } + + @Override + public List getSubValues() { + return new ArrayList<>(properties.values()); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ReferenceType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ReferenceType.java new file mode 100644 index 000000000..caaa7ba22 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/ReferenceType.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +/** + * + * @author JPEXS + */ +public class ReferenceType { + public int referenceIndex; + public Object referencedObject; + + public ReferenceType(int referenceIndex) { + this.referenceIndex = referenceIndex; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/TypedObjectType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/TypedObjectType.java new file mode 100644 index 000000000..ff6e6c7d5 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/TypedObjectType.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Typed object type. + * @author JPEXS + */ +public class TypedObjectType implements ComplexObject { + public String className; + public Map properties = new LinkedHashMap<>(); + + @Override + public String toString() { + return "TypedObjectType (" + className + ")"; + } + + @Override + public List getSubValues() { + return new ArrayList<>(properties.values()); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/XmlDocumentType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/XmlDocumentType.java new file mode 100644 index 000000000..fd152857e --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/XmlDocumentType.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; + +import com.jpexs.decompiler.flash.amf.amf3.types.*; + +/** + * AMF0 XML document type. + */ +public class XmlDocumentType implements Amf3ValueType { + + /** + * Data + */ + private String data; + + /** + * Constructor. + * @param data Data + */ + public XmlDocumentType(String data) { + this.data = data; + } + + /** + * Gets data. + * @return Data + */ + public String getData() { + return data; + } + + /** + * Sets data. + * @param data Data + */ + public void setData(String data) { + this.data = data; + } + + @Override + public String toString() { + return data; + } + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/package-info.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/package-info.java new file mode 100644 index 000000000..1a274a260 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf0/types/package-info.java @@ -0,0 +1,4 @@ +/** + * AMF0 types. + */ +package com.jpexs.decompiler.flash.amf.amf0.types; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3InputStream.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3InputStream.java index 08b3d567b..53415a61c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3InputStream.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3InputStream.java @@ -73,6 +73,11 @@ public class Amf3InputStream extends InputStream { this.is = is; } + @Override + public int available() throws IOException { + return is.available(); + } + /** * New dump level. * @param name Name @@ -424,7 +429,18 @@ public class Amf3InputStream extends InputStream { return readValue(name, serializers, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } - private Object readValue(String name, Map serializers, + /** + * Reads value + * @param name Name + * @param serializers Serializers + * @param objectTable Object table + * @param traitsTable Traits table + * @param stringTable String table + * @return Value + * @throws IOException On I/O error + * @throws NoSerializerExistsException If no serializer exists + */ + public Object readValue(String name, Map serializers, List objectTable, List traitsTable, List stringTable diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3OutputStream.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3OutputStream.java index 57d4bbdb8..23a8a079b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3OutputStream.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Amf3OutputStream.java @@ -182,7 +182,7 @@ public class Amf3OutputStream extends OutputStream { * @param stringTable String table * @throws IOException On I/O error */ - private void writeUtf8Vr(String val, List stringTable) throws IOException { + public void writeUtf8Vr(String val, List stringTable) throws IOException { int stringIndex = stringTable.indexOf(val); if (stringIndex == -1) { if (!val.isEmpty()) { @@ -336,7 +336,17 @@ public class Amf3OutputStream extends OutputStream { writeValue(object, serializers, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } - private void writeValue(Object object, Map serializers, List stringTable, List traitsTable, List objectTable) throws IOException, NoSerializerExistsException { + /** + * Writes value. + * @param object Object + * @param serializers Serializers + * @param stringTable String table + * @param traitsTable Traits table + * @param objectTable Object table + * @throws IOException On I/O error + * @throws NoSerializerExistsException If no serializer exists + */ + public void writeValue(Object object, Map serializers, List stringTable, List traitsTable, List objectTable) throws IOException, NoSerializerExistsException { if (object == BasicType.UNDEFINED) { writeU8(Marker.UNDEFINED); } else if (object == BasicType.NULL) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListMap.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListMap.java index 5c67acaf2..52e66f22b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListMap.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListMap.java @@ -20,20 +20,21 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.IdentityHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** - * Map which maintains order of keys. + * Map which maintains order of keys. Similar to LinkedHashMap. * * @param Key type * @param Value type */ public class ListMap implements Map { - private final Set orderedKeys = new ListSet<>(); + private final Set orderedKeys = new LinkedHashSet<>(); private final Map map; /** @@ -129,7 +130,7 @@ public class ListMap implements Map { @Override public Set keySet() { - return new ListSet<>(orderedKeys); + return new LinkedHashSet<>(orderedKeys); } @Override @@ -143,7 +144,7 @@ public class ListMap implements Map { @Override public Set> entrySet() { - Set> ret = new ListSet<>(); + Set> ret = new LinkedHashSet<>(); for (K key : orderedKeys) { V value = map.get(key); ret.add(new MyEntry<>(key, value)); @@ -158,7 +159,7 @@ public class ListMap implements Map { */ public static class MyEntry implements Entry { - private K key; + private final K key; private V value; public MyEntry(K key, V value) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListSet.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListSet.java deleted file mode 100644 index 0c777760b..000000000 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/ListSet.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2010-2024 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.amf.amf3; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -/** - * Set which maintains orders elements by time they were added. - * - * @param Type of element - */ -public class ListSet implements Set { - - private final List list = new ArrayList<>(); - - /** - * Constructor. - */ - public ListSet() { - - } - - /** - * Constructor. - * - * @param c Collection - */ - public ListSet(Collection c) { - addAll(c); - } - - @Override - public int size() { - return list.size(); - } - - @Override - public boolean isEmpty() { - return list.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return list.contains(o); - } - - @Override - public Iterator iterator() { - return list.iterator(); - } - - @Override - public Object[] toArray() { - return list.toArray(); - } - - @Override - public E[] toArray(E[] a) { - return (E[]) list.toArray(a); - } - - @Override - public boolean add(E e) { - if (!contains(e)) { - list.add(e); - return true; - } - return false; - } - - @Override - public boolean remove(Object o) { - return list.remove(o); - } - - @Override - public boolean containsAll(Collection c) { - return list.containsAll(c); - } - - @Override - public boolean addAll(Collection c) { - boolean modified = false; - for (E e : c) { - if (add(e)) { - modified = true; - } - } - return modified; - } - - @Override - public boolean retainAll(Collection c) { - return list.retainAll(c); - } - - @Override - public boolean removeAll(Collection c) { - return list.removeAll(c); - } - - @Override - public void clear() { - list.clear(); - } - -} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Traits.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Traits.java index c9e212c96..96666fab8 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Traits.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/Traits.java @@ -17,6 +17,8 @@ package com.jpexs.decompiler.flash.amf.amf3; import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; import java.util.Set; /** @@ -37,7 +39,7 @@ public class Traits { public Traits(String className, boolean dynamic, Collection sealedMemberNames) { this.className = className; this.dynamic = dynamic; - this.sealedMemberNames = new ListSet<>(sealedMemberNames); + this.sealedMemberNames = new LinkedHashSet<>(sealedMemberNames); } /** @@ -61,7 +63,7 @@ public class Traits { * @return Sealed member names */ public Set getSealedMemberNames() { - return new ListSet<>(sealedMemberNames); + return new LinkedHashSet<>(sealedMemberNames); } /** @@ -85,7 +87,36 @@ public class Traits { * @param sealedMemberNames Sealed member names */ public void setSealedMemberNames(Collection sealedMemberNames) { - this.sealedMemberNames = new ListSet<>(sealedMemberNames); + this.sealedMemberNames = new LinkedHashSet<>(sealedMemberNames); } + @Override + public int hashCode() { + int hash = 7; + hash = 11 * hash + Objects.hashCode(this.className); + hash = 11 * hash + (this.dynamic ? 1 : 0); + hash = 11 * hash + Objects.hashCode(this.sealedMemberNames); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Traits other = (Traits) obj; + if (this.dynamic != other.dynamic) { + return false; + } + if (!Objects.equals(this.className, other.className)) { + return false; + } + return Objects.equals(this.sealedMemberNames, other.sealedMemberNames); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/DictionaryType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/DictionaryType.java index 41ea1d3ef..1a381d15e 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/DictionaryType.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/DictionaryType.java @@ -21,6 +21,7 @@ import com.jpexs.decompiler.flash.amf.amf3.WithSubValues; import com.jpexs.decompiler.flash.exporters.amf.amf3.Amf3Exporter; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,7 @@ public class DictionaryType extends ListMap implements WithSubVa * True if keys are weak */ private final boolean weakKeys; - + /** * Constructor. * @param weakKeys True if keys are weak diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/ObjectType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/ObjectType.java index e7ed3fd11..9c6cafd86 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/ObjectType.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/amf/amf3/types/ObjectType.java @@ -17,13 +17,13 @@ package com.jpexs.decompiler.flash.amf.amf3.types; import com.jpexs.decompiler.flash.amf.amf3.ListMap; -import com.jpexs.decompiler.flash.amf.amf3.ListSet; import com.jpexs.decompiler.flash.amf.amf3.Traits; import com.jpexs.decompiler.flash.amf.amf3.WithSubValues; import com.jpexs.decompiler.flash.exporters.amf.amf3.Amf3Exporter; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -436,7 +436,7 @@ public class ObjectType implements WithSubValues, Amf3ValueType, Map keySet() { - Set ret = new ListSet<>(); + Set ret = new LinkedHashSet<>(); ret.addAll(sealedMembers.keySet()); ret.addAll(dynamicMembers.keySet()); ret.addAll(serializedMembers.keySet()); @@ -448,7 +448,7 @@ public class ObjectType implements WithSubValues, Amf3ValueType, Map sealedMembersKeySet() { - return new ListSet<>(sealedMembers.keySet()); + return new LinkedHashSet<>(sealedMembers.keySet()); } /** @@ -456,7 +456,7 @@ public class ObjectType implements WithSubValues, Amf3ValueType, Map dynamicMembersKeySet() { - return new ListSet<>(dynamicMembers.keySet()); + return new LinkedHashSet<>(dynamicMembers.keySet()); } /** @@ -464,7 +464,7 @@ public class ObjectType implements WithSubValues, Amf3ValueType, Map serializedMembersKeySet() { - return new ListSet<>(serializedMembers.keySet()); + return new LinkedHashSet<>(serializedMembers.keySet()); } @Override @@ -486,7 +486,7 @@ public class ObjectType implements WithSubValues, Amf3ValueType, Map> entrySet() { Set keys = keySet(); - Set> ret = new ListSet<>(); + Set> ret = new LinkedHashSet<>(); for (String key : keys) { ret.add(new ListMap.MyEntry<>(key, get(key))); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java index 25ae96c7d..7c800c36c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java @@ -1045,6 +1045,10 @@ public final class Configuration { @ConfigurationCategory("limit") public static ConfigurationItem maxScriptLineLength = null; + @ConfigurationDefaultString(".") + @ConfigurationDirectory + public static ConfigurationItem lastSolEditorDirectory = null; + private enum OSId { WINDOWS, OSX, UNIX } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/Amf0Exporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/Amf0Exporter.java new file mode 100644 index 000000000..9057ed854 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/Amf0Exporter.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.exporters.amf.amf0; + +import com.jpexs.decompiler.flash.amf.amf0.types.ArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.BasicType; +import com.jpexs.decompiler.flash.amf.amf0.types.DateType; +import com.jpexs.decompiler.flash.amf.amf0.types.EcmaArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.ObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.TypedObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.XmlDocumentType; +import com.jpexs.helpers.Helper; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.jpexs.decompiler.flash.amf.amf0.types.ComplexObject; +import com.jpexs.decompiler.flash.ecma.EcmaScript; + +/** + * AMF0 exporter. + * + * @author JPEXS + */ +public class Amf0Exporter { + + public static String amfMapToString( + Map map, + int level, + String newLine + ) { + List processedObjects = new ArrayList<>(); + Map referenceCount = new LinkedHashMap<>(); + Map objectAlias = new LinkedHashMap<>(); + + List objectList = new ArrayList<>(); + for (String key: map.keySet()) { + Object val = map.get(key); + populateObjects(val, referenceCount, objectList, objectAlias); + } + + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + boolean first = true; + for (String key: map.keySet()) { + if (!first) { + sb.append(",").append(newLine); + } + first = false; + sb.append(indent(level + 1)).append("\"").append(Helper.escapeActionScriptString(key)).append("\": "); + sb.append(amfToString(map.get(key), level + 1, newLine, processedObjects, referenceCount, objectAlias)); + } + sb.append(newLine); + sb.append(indent(level)).append("}"); + return sb.toString(); + } + + public static String amfToString( + Object value, + int level, + String newLine, + List processedObjects, + Map referenceCount, + Map objectAlias + ) { + String addId = ""; + if (referenceCount.containsKey(value)) { + Integer refCount = referenceCount.get(value); + if (refCount > 1 && processedObjects.contains(value)) { + return "#" + objectAlias.get(value); + } + if (refCount > 1) { + addId = indent(level + 1) + "\"id\": \"" + objectAlias.get(value) + "\"," + newLine; + } + processedObjects.add(value); + } + + + + if (value instanceof Double) { + return EcmaScript.toString(value); + } + if (value instanceof Boolean) { + return value.toString(); + } + if (value instanceof String) { + return "\"" + Helper.escapeActionScriptString((String) value) + "\""; + } + if (value instanceof ObjectType) { + ObjectType ot = (ObjectType) value; + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + sb.append(indent(level + 1)).append("\"type\": \"Object\",").append(newLine); + sb.append(addId); + membersToString(sb, ot.properties, level + 1, newLine, processedObjects, referenceCount, objectAlias); + sb.append(indent(level)).append("}"); + return sb.toString(); + } + if (value instanceof EcmaArrayType) { + EcmaArrayType eat = (EcmaArrayType) value; + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + sb.append(indent(level + 1)).append("\"type\": \"EcmaArray\",").append(newLine); + sb.append(addId); + membersToString(sb, eat.values, level + 1, newLine, processedObjects, referenceCount, objectAlias); + sb.append(indent(level)).append("}"); + return sb.toString(); + } + + if (value instanceof ArrayType) { + ArrayType at = (ArrayType) value; + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + sb.append(indent(level + 1)).append("\"type\": \"Array\",").append(newLine); + sb.append(addId); + sb.append(indent(level + 1)).append("\"values\": [").append(newLine); + boolean first = true; + for (Object val : at.values) { + if (!first) { + sb.append(", "); + } + first = false; + sb.append(amfToString(val, level + 1, newLine, processedObjects, referenceCount, objectAlias)); + } + sb.append(indent(level + 1)).append("]").append(newLine); + + sb.append(indent(level)).append("}"); + return sb.toString(); + } + + if (value instanceof TypedObjectType) { + TypedObjectType tot = (TypedObjectType) value; + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + sb.append(indent(level + 1)).append("\"type\": \"TypedObject\",").append(newLine); + sb.append(addId); + sb.append(indent(level + 1)).append("\"className\": \"").append(Helper.escapeActionScriptString(tot.className)).append("\",").append(newLine); + membersToString(sb, tot.properties, level + 1, newLine, processedObjects, referenceCount, objectAlias); + sb.append(indent(level)).append("}"); + return sb.toString(); + } + + if (value instanceof BasicType) { + return value.toString(); + } + + if (value instanceof DateType) { + DateType dt = (DateType) value; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS"); + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + sb.append(indent(level + 1)).append("\"type\": \"Date\",").append(newLine); + sb.append(indent(level + 1)).append("\"value\": \"").append(sdf.format(dt.toDate())).append("\",").append(newLine); + sb.append(indent(level + 1)).append("\"timezone\": ").append(dt.getTimezone()).append(newLine); + sb.append(indent(level)).append("}"); + return sb.toString(); + } + + if (value instanceof XmlDocumentType) { + XmlDocumentType xdt = (XmlDocumentType) value; + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + sb.append(indent(level + 1)).append("\"type\": \"XMLDocument\",").append(newLine); + sb.append(indent(level + 1)).append("\"data\": \"").append(Helper.escapeActionScriptString(xdt.getData())).append("\"").append(newLine); + sb.append(indent(level)).append("}"); + return sb.toString(); + } + + return "unknown"; + } + + private static void membersToString( + StringBuilder sb, + Map members, + int level, + String newLine, + List processedObjects, + Map referenceCount, + Map objectAlias) { + sb.append(indent(level)).append("\"members\": {").append(newLine); + boolean first = true; + for (String key : members.keySet()) { + if (!first) { + sb.append(",").append(newLine); + } + first = false; + sb.append(indent(level + 1)).append("\"").append(Helper.escapeActionScriptString(key)).append("\": "); + sb.append(amfToString(members.get(key), level + 1, newLine, processedObjects, referenceCount, objectAlias)); + } + sb.append(newLine); + sb.append(indent(level)).append("}").append(newLine); + + } + + private static String indent(int level) { + String na = ""; + for (int i = 0; i < level; i++) { + na += " "; + } + return na; + } + + /** + * Populates all object instances and their references and generates aliases + * + * @param object Object to be populated + * @param referenceCount Result: Map of reference number + * @param objectList Result: List of all found object instances + * @param objectAlias Result: Map of assigned object names + */ + public static void populateObjects(Object object, Map referenceCount, List objectList, Map objectAlias) { + if (((List) Arrays.asList(String.class, Double.class, BasicType.class, Boolean.class)).contains(object.getClass())) { + return; + } + if (object instanceof BasicType) { + return; + } + int prevRef = 0; + if (referenceCount.containsKey(object)) { + prevRef = referenceCount.get(object); + } + referenceCount.put(object, prevRef + 1); + if (prevRef == 0) { + if (object instanceof ComplexObject) { + List subvalues = ((ComplexObject) object).getSubValues(); + for (Object o : subvalues) { + populateObjects(o, referenceCount, objectList, objectAlias); + } + } + objectList.add(object); + objectAlias.put(object, "obj" + objectList.size()); + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/package-info.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/package-info.java new file mode 100644 index 000000000..a56e6b77c --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf0/package-info.java @@ -0,0 +1,4 @@ +/** + * AMF0 export. + */ +package com.jpexs.decompiler.flash.exporters.amf.amf0; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf3/Amf3Exporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf3/Amf3Exporter.java index dd5b58c08..a07b92572 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf3/Amf3Exporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/amf/amf3/Amf3Exporter.java @@ -115,6 +115,30 @@ public class Amf3Exporter { populateObjects(amfValue, refCount, objectList, objectAlias); return amfToString(indentStr, newLine, new ArrayList<>(), 0, amfValue, refCount, objectAlias); } + + public static String amfMapToString(Map map, String indentStr, String newLine, int level) { + Map refCount = new HashMap<>(); + List objectList = new ArrayList<>(); + Map objectAlias = new HashMap<>(); + for (Object val : map.values()) { + populateObjects(val, refCount, objectList, objectAlias); + } + List processedObjects = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + sb.append("{").append(newLine); + boolean first = true; + for (String key: map.keySet()) { + if (!first) { + sb.append(", ").append(newLine); + } + first = false; + sb.append(indent(level + 1)).append("\"").append(Helper.escapeActionScriptString(key)).append("\": "); + sb.append(amfToString(indentStr, newLine, processedObjects, level + 1, map.get(key), refCount, objectAlias)); + } + sb.append(newLine); + sb.append(indent(level)).append("}"); + return sb.toString(); + } /** * Processes one level of object and converts it to string @@ -226,7 +250,7 @@ public class Amf3Exporter { for (String key : ot.dynamicMembersKeySet()) { Object val = ot.getDynamicMember(key); ret.append(indent(level + 2)).append(amfToString(indentStr, newLine, processedObjects, level + 2, key, referenceCount, objectAlias)); - ret.append(":"); + ret.append(": "); ret.append(amfToString(indentStr, newLine, processedObjects, level + 2, val, referenceCount, objectAlias)); if (i < ot.dynamicMembersSize() - 1) { ret.append(","); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/Amf0Importer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/Amf0Importer.java new file mode 100644 index 000000000..d83e6c259 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/Amf0Importer.java @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.importers.amf.amf0; + +import com.jpexs.decompiler.flash.amf.amf0.types.ArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.BasicType; +import com.jpexs.decompiler.flash.amf.amf0.types.DateType; +import com.jpexs.decompiler.flash.amf.amf0.types.EcmaArrayType; +import com.jpexs.decompiler.flash.amf.amf0.types.ObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.TypedObjectType; +import com.jpexs.decompiler.flash.amf.amf0.types.XmlDocumentType; +import com.jpexs.decompiler.flash.importers.amf.amf3.Amf3Lexer; +import com.jpexs.decompiler.flash.importers.amf.amf3.Amf3ParseException; +import com.jpexs.decompiler.flash.importers.amf.amf3.ParsedSymbol; +import com.jpexs.decompiler.flash.importers.amf.amf3.SymbolType; +import com.jpexs.helpers.Helper; +import java.io.IOException; +import java.io.StringReader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * AMF0 importer. + */ +public class Amf0Importer { + + private Amf3Lexer lexer; + + /** + * Constructor. + */ + public Amf0Importer() { + } + + private ParsedSymbol lex() throws IOException, Amf3ParseException { + ParsedSymbol ret = lexer.lex(); + return ret; + } + + private void pushback(ParsedSymbol s) { + lexer.pushback(s); + } + + private void expected(ParsedSymbol symb, int line, Object... expected) throws IOException, Amf3ParseException { + boolean found = false; + for (Object t : expected) { + if (symb.type == t) { + found = true; + } + if (symb.group == t) { + found = true; + } + } + if (!found) { + String expStr = ""; + boolean first = true; + for (Object e : expected) { + if (!first) { + expStr += " or "; + } + expStr += e; + first = false; + } + throw new Amf3ParseException("" + expStr + " expected but " + symb.type + " found", line); + } + } + + private ParsedSymbol expectedType(Object... type) throws IOException, Amf3ParseException { + ParsedSymbol symb = lex(); + expected(symb, lexer.yyline(), type); + return symb; + } + + private JsArray parseArray(Map objectTable) throws IOException, Amf3ParseException { + expectedType(SymbolType.BRACKET_OPEN); + List arrayVals = new ArrayList<>(); + ParsedSymbol s = lex(); + if (!s.isType(SymbolType.BRACKET_CLOSE)) { + pushback(s); + arrayVals.add(value(objectTable)); + s = lex(); + while (s.isType(SymbolType.COMMA)) { + arrayVals.add(value(objectTable)); + s = lex(); + } + pushback(s); + } + expectedType(SymbolType.BRACKET_CLOSE); + return new JsArray(arrayVals); + } + + private class JsArray { + + private List values = new ArrayList<>(); + + public JsArray() { + } + + public JsArray(List values) { + this.values = values; + } + + public void add(Object value) { + values.add(value); + } + + public List getValues() { + return values; + } + + } + + private class JsObject { + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (Object key : values.keySet()) { + sb.append(key).append(":").append("?").append(",\r\n"); + } + sb.append("}"); + return sb.toString(); + } + + private final Map values = new LinkedHashMap<>(); + + public Object remove(Object key) { + return values.remove(key); + } + + public Set keySet() { + return values.keySet(); + } + + public Object get(Object key) { + return values.get(key); + } + + public void put(Object key, Object value) { + values.put(key, value); + } + + public String getString(Object key) throws Amf3ParseException { + return (String) getRequired(key, "String"); + } + + public Boolean getBoolean(Object key) throws Amf3ParseException { + return (Boolean) getRequired(key, "Boolean"); + } + + public JsObject getJsObject(Object key) throws Amf3ParseException { + return (JsObject) getRequired(key, "JsObject"); + } + + public List getJsArrayOfObject(Object key) throws Amf3ParseException { + return getJsArray(key).getValues(); + } + + @SuppressWarnings("unchecked") + public List getJsArrayOfString(Object key) throws Amf3ParseException { + return (List) getJsArray(key, "String"); + } + + @SuppressWarnings("unchecked") + public List getJsArrayOfInt(Object key) throws Amf3ParseException { + return (List) getJsArray(key, "int"); + } + + @SuppressWarnings("unchecked") + + public List getJsArrayOfUint(Object key) throws Amf3ParseException { + return (List) getJsArray(key, "uint"); + } + + @SuppressWarnings("unchecked") + public List getJsArrayOfNumber(Object key) throws Amf3ParseException { + return (List) getJsArray(key, "Number"); + } + + public JsArray getJsArray(Object key) throws Amf3ParseException { + return (JsArray) getRequired(key, "JsArray"); + } + + public List getJsArray(Object key, String valueType) throws Amf3ParseException { + JsArray jsArr = (JsArray) getRequired(key, "JsArray"); + switch (valueType) { + case "String": + List stringList = new ArrayList<>(); + for (Object v : jsArr.getValues()) { + String sv = null; + if (v instanceof String) { + sv = (String) v; + } else { + throw new Amf3ParseException("Not String: " + v, 0); + } + stringList.add(sv); + } + return stringList; + case "int": + case "uint": + List longList = new ArrayList<>(); + for (Object v : jsArr.getValues()) { + Long lv = null; + if (v instanceof Long) { + lv = (Long) v; + } else { + throw new Amf3ParseException("Not an Integer value: " + v, 0); + } + if (valueType.equals("uint") && lv < 0) { + throw new Amf3ParseException("Not an Unsigned Integer value: " + v, 0); + } + longList.add(lv); + } + return longList; + case "Number": + List doubleList = new ArrayList<>(); + for (Object v : jsArr.getValues()) { + Double cv = null; + if (v instanceof Long) { + cv = (double) (long) (Long) v; + } else if (v instanceof Double) { + cv = (Double) v; + } else { + throw new Amf3ParseException("Not a Number: " + v, 0); + } + doubleList.add(cv); + } + return doubleList; + default: + throw new Amf3ParseException("Unsupported array value type: " + valueType, 0); + } + } + + public Long getLong(Object key) throws Amf3ParseException { + return (Long) getRequired(key, "Long"); + } + + public Double getDouble(Object key) throws Amf3ParseException { + return (Double) getRequired(key, "Double"); + } + + public Object getRequired(Object key, String requiredType) throws Amf3ParseException { + if (!containsKey(key)) { + throw new Amf3ParseException("\"" + key + "\" is missing", 0); + } + Object val = get(key); + boolean typeMatches = true; + if (requiredType != null) { + switch (requiredType) { + case "String": + typeMatches = val instanceof String; + break; + case "Long": + typeMatches = val instanceof Long; + break; + case "JsObject": + typeMatches = val instanceof JsObject; + break; + case "JsArray": + typeMatches = val instanceof JsArray; + break; + case "Boolean": + typeMatches = val instanceof Boolean; + break; + } + } + if (!typeMatches) { + throw new Amf3ParseException("\"" + key + "\" value must be of type " + requiredType, 0); + } + return val; + } + + public boolean containsKey(Object key) { + return values.containsKey(key); + } + + public void resolve(Object key, Map objectTable, boolean allowTypedObject) throws Amf3ParseException { + Object val = values.get(key); + Object resolved = resolveObjects(val, objectTable, allowTypedObject); + values.put(key, resolved); + } + + public List stringKeys() { + List ret = new ArrayList<>(); + for (Object key : values.keySet()) { + if (key instanceof String) { + ret.add((String) key); + } + } + return ret; + } + + public Map getAll() { + return values; + } + + public Map getStringMapped() { + Map ret = new LinkedHashMap<>(); + for (Object key : values.keySet()) { + if (key instanceof String) { + String keyStr = (String) key; + ret.put(keyStr, values.get(key)); + } + } + return ret; + } + } + + private JsObject parseObject(Map objectTable) throws IOException, Amf3ParseException { + JsObject ret = new JsObject(); + + expectedType(SymbolType.CURLY_OPEN); + ParsedSymbol s = lex(); + if (!s.isType(SymbolType.CURLY_CLOSE)) { + pushback(s); + do { + Object key = value(objectTable); + expectedType(SymbolType.COLON); + Object value = value(objectTable); + ret.put(key, value); + if ("id".equals(key)) { + if (!(value instanceof String)) { + throw new Amf3ParseException("id must be string value", lexer.yyline()); + } + objectTable.put((String) value, BasicType.UNDEFINED); + } + s = lex(); + } while (s.isType(SymbolType.COMMA)); + } + pushback(s); + expectedType(SymbolType.CURLY_CLOSE); + return ret; + } + + private Object resolveObjects(Object object, Map objectTable, boolean allowTypedObject) throws Amf3ParseException { + Object resultObject = object; + if (object instanceof JsArray) { + JsArray jsa = (JsArray) object; + JsArray ret = new JsArray(); + for (int i = 0; i < jsa.values.size(); i++) { + ret.values.add(resolveObjects(jsa.values.get(i), objectTable, true)); + } + resultObject = ret; + } else if (object instanceof JsObject) { + if (allowTypedObject) { + JsObject typedObject = (JsObject) object; + if (typedObject.containsKey("type")) { + String typeStr = typedObject.getString("type"); + String id = typedObject.containsKey("id") ? typedObject.getString("id") : null; + switch (typeStr) { + case "Date": + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS"); + String dateStr = typedObject.getString("value"); + int timeZone = (int) (long) (double) typedObject.getDouble("timezone"); + try { + resultObject = new DateType((double) sdf.parse(dateStr).getTime(), timeZone); + } catch (ParseException ex) { + throw new Amf3ParseException("Invalid date format: " + dateStr, lexer.yyline()); + } + break; + case "XMLDocument": + resultObject = new XmlDocumentType(typedObject.getString("data")); + break; + case "Object": + ObjectType ot = new ObjectType(); + typedObject.resolve("members", objectTable, false); + ot.properties = typedObject.getJsObject("members").getStringMapped(); + resultObject = ot; + break; + case "TypedObject": + TypedObjectType tot = new TypedObjectType(); + tot.className = typedObject.getString("className"); + typedObject.resolve("members", objectTable, false); + tot.properties = typedObject.getJsObject("members").getStringMapped(); + resultObject = tot; + break; + case "EcmaArray": + EcmaArrayType eat = new EcmaArrayType(); + typedObject.resolve("members", objectTable, false); + eat.values = typedObject.getJsObject("members").getStringMapped(); + resultObject = eat; + break; + case "Array": + ArrayType at = new ArrayType(); + typedObject.resolve("values", objectTable, false); + at.values = typedObject.getJsArray("values").getValues(); + resultObject = at; + break; + default: + throw new Amf3ParseException("Unknown object type: " + typeStr, lexer.yyline()); + } + if (id != null) { + objectTable.put(id, resultObject); + } + } + } else { //not allowTypeObject + JsObject jsObject = (JsObject) object; + for (Object key : jsObject.keySet()) { + Object val = jsObject.get(key); + //Object resKey = resolveObjects(key, objectTable, true); + Object resVal = resolveObjects(val, objectTable, true); + //jsObject.remove(key); + jsObject.put(key, resVal); + } + resultObject = jsObject; + } + } + return resultObject; + } + + private Map map(Map objectTable) throws IOException, Amf3ParseException { + Map result = new HashMap<>(); + expectedType(SymbolType.CURLY_OPEN); + ParsedSymbol s; + do { + s = lex(); + if (!s.isType(SymbolType.STRING)) { + break; + } + String key = (String) s.value; + expectedType(SymbolType.COLON); + result.put(key, value(objectTable)); + s = lex(); + } while (s.type == SymbolType.COMMA); + + expected(s, lexer.yyline(), SymbolType.CURLY_CLOSE); + return result; + } + + private Object value(Map objectTable) throws IOException, Amf3ParseException { + ParsedSymbol s = lex(); + switch (s.type) { + case CURLY_OPEN: + pushback(s); + return parseObject(objectTable); + case BRACKET_OPEN: + pushback(s); + return parseArray(objectTable); + case STRING: + case DOUBLE: + return s.value; + case INTEGER: + return (double) (long) (Long) s.value; + case UNDEFINED: + return BasicType.UNDEFINED; + case NULL: + return BasicType.NULL; + case UNKNOWN: + return BasicType.UNKNOWN; + case TRUE: + return Boolean.TRUE; + case FALSE: + return Boolean.FALSE; + case REFERENCE: + String referencedId = (String) s.value; + return new ReferencedObjectType(referencedId); + default: + throw new Amf3ParseException("Unexpected symbol: " + s, lexer.yyline()); + } + } + + /** + * Deeply replace all ReferencedObjectType with the correct value + * + * @param object Object + * @param objectsTable Objects table + * @return Replaced object + */ + private Object replaceReferences(Object object, Map objectsTable) throws Amf3ParseException { + if (object instanceof ReferencedObjectType) { + String key = ((ReferencedObjectType) object).key; + if (!objectsTable.containsKey(key)) { + throw new Amf3ParseException("Reference to undefined object: #" + key, 0); + } + return objectsTable.get(key); + } else if (object instanceof ObjectType) { + ObjectType ot = (ObjectType) object; + for (String key : ot.properties.keySet()) { + ot.properties.put(key, replaceReferences(ot.properties.get(key), objectsTable)); + } + } else if (object instanceof TypedObjectType) { + TypedObjectType tot = (TypedObjectType) object; + for (String key : tot.properties.keySet()) { + tot.properties.put(key, replaceReferences(tot.properties.get(key), objectsTable)); + } + } else if (object instanceof EcmaArrayType) { + EcmaArrayType eat = (EcmaArrayType) object; + for (String key : eat.values.keySet()) { + eat.values.put(key, replaceReferences(eat.values.get(key), objectsTable)); + } + } else if (object instanceof ArrayType) { + ArrayType at = (ArrayType) object; + for (int i = 0; i < at.values.size(); i++) { + at.values.set(i, replaceReferences(at.values.get(i), objectsTable)); + } + } + + return object; + } + + private class ReferencedObjectType { + + private final String key; + + public ReferencedObjectType(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + @Override + public String toString() { + return "#" + key; + } + } + + /** + * Convert AMF3 string to object + * + * @param val AMF3 string + * @return Object + * @throws IOException On I/O error + * @throws Amf3ParseException On parse error + */ + public Object stringToAmf(String val) throws IOException, Amf3ParseException { + lexer = new Amf3Lexer(new StringReader(val)); + Map objectsTable = new HashMap<>(); + List references = new ArrayList<>(); + Object result = value(objectsTable); + Object resultResolved = resolveObjects(result, objectsTable, true); + Object resultNoRef = replaceReferences(resultResolved, objectsTable); + return resultNoRef; + } + + /** + * Convert AMF3 map string to object + * + * @param val AMF3 string + * @return Object + * @throws IOException On I/O error + * @throws Amf3ParseException On parse error + */ + public Map stringToAmfMap(String val) throws IOException, Amf3ParseException { + lexer = new Amf3Lexer(new StringReader(val)); + Map objectsTable = new HashMap<>(); + List references = new ArrayList<>(); + Map result = map(objectsTable); + for (String key : result.keySet()) { + Object resultResolved = resolveObjects(result.get(key), objectsTable, true); + result.put(key, resultResolved); + } + + for (String key : result.keySet()) { + Object resultNoRef = replaceReferences(result.get(key), objectsTable); + result.put(key, resultNoRef); + } + + return result; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/package-info.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/package-info.java new file mode 100644 index 000000000..569935ac7 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf0/package-info.java @@ -0,0 +1,4 @@ +/** + * AMF0 importer. + */ +package com.jpexs.decompiler.flash.importers.amf.amf0; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf3/Amf3Importer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf3/Amf3Importer.java index 120f532cf..9c8785cb2 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf3/Amf3Importer.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/amf/amf3/Amf3Importer.java @@ -460,8 +460,27 @@ public class Amf3Importer { } } return resultObject; + } + + private Map map(Map objectTable) throws IOException, Amf3ParseException { + Map result = new HashMap<>(); + expectedType(SymbolType.CURLY_OPEN); + ParsedSymbol s; + do { + s = lex(); + if (!s.isType(SymbolType.STRING)) { + break; + } + String key = (String) s.value; + expectedType(SymbolType.COLON); + result.put(key, value(objectTable)); + s = lex(); + } while(s.type == SymbolType.COMMA); + + expected(s, lexer.yyline(), SymbolType.CURLY_CLOSE); + return result; } - + private Object value(Map objectTable) throws IOException, Amf3ParseException { ParsedSymbol s = lex(); switch (s.type) { @@ -581,4 +600,30 @@ public class Amf3Importer { Object resultNoRef = replaceReferences(resultResolved, objectsTable); return resultNoRef; } + + /** + * Convert AMF3 map string to object + * + * @param val AMF3 string + * @return Object + * @throws IOException On I/O error + * @throws Amf3ParseException On parse error + */ + public Map stringToAmfMap(String val) throws IOException, Amf3ParseException { + lexer = new Amf3Lexer(new StringReader(val)); + Map objectsTable = new HashMap<>(); + List references = new ArrayList<>(); + Map result = map(objectsTable); + for (String key: result.keySet()) { + Object resultResolved = resolveObjects(result.get(key), objectsTable, true); + result.put(key, resultResolved); + } + + for (String key: result.keySet()) { + Object resultNoRef = replaceReferences(result.get(key), objectsTable); + result.put(key, resultNoRef); + } + + return result; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/FilePathTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/FilePathTag.java new file mode 100644 index 000000000..a0437a282 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/FilePathTag.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.sol; + +import com.jpexs.decompiler.flash.amf.amf0.Amf0InputStream; +import com.jpexs.decompiler.flash.amf.amf0.Amf0OutputStream; +import com.jpexs.helpers.MemoryInputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * + * @author JPEXS + */ +public class FilePathTag extends Tag { + + public static final int ID = 3; + + public String filePath; + + public FilePathTag(byte[] data, boolean forceWriteAsLong) { + super(ID, "DefineFilePath", data, forceWriteAsLong); + } + + @Override + public void readData() throws IOException { + Amf0InputStream is = new Amf0InputStream(new MemoryInputStream(data)); + int filePathLen = is.readU16("filePath"); + filePath = new String(is.readBytes(filePathLen), "UTF-8"); + } + + @Override + public void writeData(OutputStream os) throws IOException { + Amf0OutputStream aos = new Amf0OutputStream(os); + byte[] filePathData = filePath.getBytes("UTF-8"); + aos.writeU16(filePathData.length); + aos.writeBytes(filePathData); + } + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/LsoTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/LsoTag.java new file mode 100644 index 000000000..8eccecca6 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/LsoTag.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.sol; + +import com.jpexs.decompiler.flash.amf.amf0.Amf0InputStream; +import com.jpexs.decompiler.flash.amf.amf0.Amf0OutputStream; +import com.jpexs.decompiler.flash.amf.amf3.Amf3InputStream; +import com.jpexs.decompiler.flash.amf.amf3.Amf3OutputStream; +import com.jpexs.decompiler.flash.amf.amf3.NoSerializerExistsException; +import com.jpexs.decompiler.flash.amf.amf3.Traits; +import com.jpexs.helpers.MemoryInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public class LsoTag extends Tag { + + public static final int ID = 2; + + public String fileName; + public int amfVersion; + public Map amfValues = new LinkedHashMap<>(); + + public LsoTag(byte[] data, boolean forceWriteAsLong) { + super(ID, "DefineLso", data, forceWriteAsLong); + } + + public LsoTag(String fileName, int amfVersion, Map amfValues) { + super(ID, "DefineLso", new byte[0], true); + this.fileName = fileName; + this.amfVersion = amfVersion; + this.amfValues = amfValues; + } + + @Override + public void readData() throws IOException { + Amf0InputStream is = new Amf0InputStream(new MemoryInputStream(data)); + + byte[] expectedSig = new byte[]{'T', 'C', 'S', 'O'}; + byte[] actualSig = is.readBytes(4); + if (!Arrays.equals(actualSig, expectedSig)) { + throw new IllegalArgumentException("Not a SOL file - invalid signature"); + } + is.skip(6); //00 04 00 00 00 00 + int filenameLen = is.readU16("filenameLen"); + fileName = new String(is.readBytes(filenameLen), "UTF-8"); + + amfVersion = (int) is.readU32("amfVersion"); + if (amfVersion != 0 && amfVersion != 3) { + throw new IllegalArgumentException("Unsupported AMF version"); + } + byte[] amfData = is.readBytes(is.available()); + if (amfVersion == 0) { + Amf0InputStream ais = new Amf0InputStream(new MemoryInputStream(amfData)); + List complexObjects = new ArrayList<>(); + while (ais.available() > 0) { + String varName = ais.readUtf8("varName"); + try { + Object varValue = ais.readValue("varValue"); + amfValues.put(varName, varValue); + } catch (NoSerializerExistsException ex) { + throw new IllegalArgumentException("Serializer for class " + ex.getClassName() + " not found"); + } + ais.read(); //ending byte + } + ais.resolveMapReferences(amfValues); + } + + if (amfVersion == 3) { + Amf3InputStream ais = new Amf3InputStream(new MemoryInputStream(amfData)); + List objectsTable = new ArrayList<>(); + List traitsTable = new ArrayList<>(); + List stringTable = new ArrayList<>(); + + while (ais.available() > 0) { + String varName = ais.readUtf8Vr("varName", new ArrayList<>()); + try { + Object varValue = ais.readValue("varValue", new HashMap<>(), objectsTable, traitsTable, stringTable); + amfValues.put(varName, varValue); + } catch (NoSerializerExistsException ex) { + throw new IllegalArgumentException("Serializer for class " + ex.getClassName() + " not found"); + } + ais.read(); //ending byte + } + } + + } + + @Override + public void writeData(OutputStream os) throws IOException { + Amf0OutputStream aos = new Amf0OutputStream(os); + aos.write(new byte[] {'T', 'C', 'S', 'O'}); + aos.write(new byte[] {0x00,0x04,0x00,0x00,0x00,0x00}); + byte[] fileNameData = fileName.getBytes("UTF-8"); + aos.writeU16(fileNameData.length); + aos.write(fileNameData); + aos.writeU32(amfVersion); + + if (amfVersion == 0) { + List complexObjects = new ArrayList<>(); + for(String key: amfValues.keySet()) { + aos.writeUtf8(key); + aos.writeValue(amfValues.get(key), complexObjects); + aos.write(0); + } + } + if (amfVersion == 3) { + Amf3OutputStream a3os = new Amf3OutputStream(os); + List stringTable = new ArrayList<>(); + List traitTable = new ArrayList<>(); + List objectTable = new ArrayList<>(); + for(String key: amfValues.keySet()) { + try { + a3os.writeUtf8Vr(key, new ArrayList<>()); //Intentionally not using string table + a3os.writeValue(amfValues.get(key), new HashMap<>(), stringTable, traitTable, objectTable); + } catch (NoSerializerExistsException ex) { + throw new IllegalArgumentException("Serializer not found for class " + ex.getClassName()); + } + aos.write(0); + } + } + + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/SolFile.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/SolFile.java new file mode 100644 index 000000000..730909648 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/SolFile.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.sol; + +import com.jpexs.decompiler.flash.EndOfStreamException; +import com.jpexs.decompiler.flash.amf.amf0.Amf0OutputStream; +import com.jpexs.decompiler.flash.exporters.amf.amf0.Amf0Exporter; +import com.jpexs.decompiler.flash.exporters.amf.amf3.Amf3Exporter; +import com.jpexs.decompiler.flash.importers.amf.amf0.Amf0Importer; +import com.jpexs.decompiler.flash.importers.amf.amf3.Amf3Importer; +import com.jpexs.decompiler.flash.importers.amf.amf3.Amf3ParseException; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author JPEXS + */ +public class SolFile { + + private DataInputStream is; + + private long pos = 0; + + private List tags = new ArrayList<>(); + + public SolFile(InputStream is) throws IOException { + this.is = new DataInputStream(is); + readTags(); + } + + public SolFile(String fileName, int amfVersion, Map amfValues) { + tags.add(new LsoTag(fileName, amfVersion, amfValues)); + } + + public void writeTo(OutputStream os) throws IOException { + for (Tag t: tags) { + writeTag(os, t); + } + } + + private void readTags() throws IOException { + while (is.available() > 0) { + Tag t = readTag(); + t.readData(); + tags.add(t); + } + } + + private int readInternal() throws IOException { + int ret = is.read(); + if (ret == -1) { + throw new EndOfStreamException(); + } + pos++; + return ret; + } + + private int readUI16() throws IOException { + int b1 = readInternal(); + int b2 = readInternal(); + int ret = (b1 << 8) + b2; + return ret; + } + + private long readUI32() throws IOException { + int b1 = readInternal(); + int b2 = readInternal(); + int b3 = readInternal(); + int b4 = readInternal(); + + return ((b1 << 24) + (b2 << 16) + (b3 << 8) + b4) & 0xffffffff; + } + + private byte[] readBytes(int count) throws IOException { + byte[] ret = new byte[count]; + try { + is.readFully(ret); + } catch (EOFException eof) { + throw new EndOfStreamException(); + } + pos += count; + return ret; + } + + private void skipBytes(int count) throws IOException { + is.skip(count); + pos += count; + } + + private void writeTag(OutputStream os, Tag t) throws IOException{ + Amf0OutputStream aos = new Amf0OutputStream(os); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + t.writeData(baos); + int contentLength = baos.size(); + if (contentLength > 0x3F || t.forceWriteAsLong) { + aos.writeU16((t.getTagType() << 6) | 0x3F); + aos.writeU32(baos.size()); + } else { + aos.writeU16((t.getTagType() << 6) | contentLength); + } + aos.writeBytes(baos.toByteArray()); + } + + private Tag readTag() throws IOException { + long headerPos = pos; + int tagTypeAndLength = readUI16(); + int contentLength = tagTypeAndLength & 0x3F; + boolean writeAsLong = false; + if (contentLength == 0x3F) { + contentLength = (int) readUI32(); + writeAsLong = true; + } + int tagType = tagTypeAndLength >> 6; + byte[] data = readBytes(contentLength); + + switch (tagType) { + case LsoTag.ID: + return new LsoTag(data, writeAsLong); + case FilePathTag.ID: + return new FilePathTag(data, writeAsLong); + default: + return new UnknownTag(tagType, data, writeAsLong); + } + } + + public List getTags() { + return tags; + } + + private LsoTag getLsoTag() throws IOException { + for (Tag t : getTags()) { + if (t instanceof LsoTag) { + return (LsoTag) t; + } + } + return null; + } + + public int getAmfVersion() throws IOException { + LsoTag lsoTag = getLsoTag(); + if (lsoTag == null) { + return -1; + } + return lsoTag.amfVersion; + } + + public String getFileName() throws IOException { + LsoTag lsoTag = getLsoTag(); + if (lsoTag == null) { + return null; + } + return lsoTag.fileName; + } + + public Map getAmfValues() throws IOException { + LsoTag lsoTag = getLsoTag(); + if (lsoTag == null) { + return new HashMap<>(); + } + return lsoTag.amfValues; + } + + public static void main(String[] args) throws FileNotFoundException, IOException { + SolFile sol0 = new SolFile(new FileInputStream("testdata/sharedobjects/data/amf0test.sol")); + sol0.writeTo(new FileOutputStream("testdata/sharedobjects/data/out/amf0test.sol")); + for (Tag t : sol0.getTags()) { + if (t instanceof LsoTag) { + LsoTag lt = (LsoTag) t; + String amf0string = Amf0Exporter.amfMapToString(lt.amfValues, 0, "\r\n"); + Amf0Importer importer = new Amf0Importer(); + try { + Map imported = importer.stringToAmfMap(amf0string); + String amf0stringNew = Amf0Exporter.amfMapToString(imported, 0, "\r\n"); + System.err.println("same0 = " + amf0stringNew.equals(amf0string)); + } catch (Amf3ParseException ex) { + ex.printStackTrace(); + } + } + } + + SolFile sol3 = new SolFile(new FileInputStream("testdata/sharedobjects/data/amf3test.sol")); + sol3.writeTo(new FileOutputStream("testdata/sharedobjects/data/out/amf3test.sol")); + for (Tag t : sol3.getTags()) { + if (t instanceof LsoTag) { + LsoTag lt = (LsoTag) t; + String amf3string = Amf3Exporter.amfMapToString(lt.amfValues, " ", "\r\n", 0); + Amf3Importer importer = new Amf3Importer(); + try { + Map imported = importer.stringToAmfMap(amf3string); + String amf3stringNew = Amf3Exporter.amfMapToString(imported, " ", "\r\n", 0);; + System.err.println("same3 = " + amf3stringNew.equals(amf3string)); + } catch (Amf3ParseException ex) { + ex.printStackTrace(); + } + } + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/Tag.java new file mode 100644 index 000000000..c95069564 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/Tag.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.sol; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * SOL file tag. + * @author JPEXS + */ +public abstract class Tag { + protected boolean forceWriteAsLong; + private final int tagType; + private final String tagName; + protected byte[] data; + + public Tag(int tagType, String tagName, byte[] data, boolean forceWriteAsLong) { + this.tagType = tagType; + this.tagName = tagName; + this.data = data; + this.forceWriteAsLong = forceWriteAsLong; + } + + public abstract void readData() throws IOException; + + public abstract void writeData(OutputStream os) throws IOException; + + public int getTagType() { + return tagType; + } + + public boolean isForceWriteAsLong() { + return forceWriteAsLong; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/UnknownTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/UnknownTag.java new file mode 100644 index 000000000..3ddbab004 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/UnknownTag.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.sol; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * + * @author JPEXS + */ +public class UnknownTag extends Tag { + + public UnknownTag(int tagType, byte[] data, boolean forceWriteAsLong) { + super(tagType, "Unknown(" + tagType + ")", data, forceWriteAsLong); + } + + @Override + public void readData() { + + } + + @Override + public void writeData(OutputStream os) throws IOException { + os.write(data); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/package-info.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/package-info.java new file mode 100644 index 000000000..745f7aeb0 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/sol/package-info.java @@ -0,0 +1,4 @@ +/** + * Reading Local Shared Objects. + */ +package com.jpexs.decompiler.flash.sol; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/Tag.java index ea8efe174..93fdc2e75 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/Tag.java @@ -21,7 +21,6 @@ import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.SWFInputStream; import com.jpexs.decompiler.flash.SWFOutputStream; import com.jpexs.decompiler.flash.abc.CopyOutputStream; -import com.jpexs.decompiler.flash.amf.amf3.ListSet; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.tags.base.BoundedTag; import com.jpexs.decompiler.flash.tags.base.CharacterIdTag; @@ -794,7 +793,7 @@ public abstract class Tag implements NeedsCharacters, Exportable, Serializable { * @return Missing needed characters */ public Set getMissingNeededCharacters(Set needed) { - Set needed2 = new ListSet<>(needed); + Set needed2 = new LinkedHashSet<>(needed); if (needed2.isEmpty()) { return new LinkedHashSet<>(); } diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/Main.as b/libsrc/ffdec_lib/testdata/sharedobjects/Main.as new file mode 100644 index 000000000..b1634a0d5 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/Main.as @@ -0,0 +1,13 @@ +package { + + import flash.display.MovieClip; + + public class Main extends MovieClip { + + public function Main() { + } + + + } + +} diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/MyClass.as b/libsrc/ffdec_lib/testdata/sharedobjects/MyClass.as new file mode 100644 index 000000000..c22f60d52 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/MyClass.as @@ -0,0 +1,12 @@ +package { + public class MyClass { + + public var a:int = 1; + public var b:int = 2; + + public function MyClass() { + // constructor code + } + + } +} \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/data/amf0test.sol b/libsrc/ffdec_lib/testdata/sharedobjects/data/amf0test.sol new file mode 100644 index 0000000000000000000000000000000000000000..8cbdf042644433b178d099169e24f8f182ced9ca GIT binary patch literal 70385 zcmeIuy>1gh5CGu0{5g?>_z|L_;0+R}D6IUDh6aHIFK~{}kCmi(ZM8S_&3rei>rpAIx@2`+ zJlwWQJ&lXUzwp+gJQ+wv2pC?cH+w(PZ78P1?oTVrbIj zq$HD*@+Xc{ZkOH7YUmf;&2d!sZ!ZM_0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5Fqg11>!K})zB}xo1GZHY}$7D)LG-vhDj9=vEUq8Kx`1vJ9Dc5~p=fl$8jp}qKsyeEt<(NkGtQ^-7 HN1N{#S=__2 literal 0 HcmV?d00001 diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/data/amf3test.sol b/libsrc/ffdec_lib/testdata/sharedobjects/data/amf3test.sol new file mode 100644 index 0000000000000000000000000000000000000000..d43561122ae6565130d49e8ba4978e2ae7a17fb4 GIT binary patch literal 70270 zcmeIuy>8P`7zSXU#!ZZ)3c*rUFd#9b4lE2&QR%{hK!O`^Y9})Cv6175o|%QaV&Y=B zgO0dd1LN~f-|zBdzq0W3?#s>Rk6E5Qq^RQV_0$}uxn7*bR990~uETju$G&cMZQs<{ zvzVszt*M9Y*XDLAUtWAjH(hl&Tz74CSX7H`9`cj + + + sharedobjects + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + Get Adobe Flash player + + + + + +
+ + diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.swf b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.swf new file mode 100644 index 0000000000000000000000000000000000000000..065c94ae0de59aaa5f162aeaf6300b34d77583c9 GIT binary patch literal 21507 zcmV(rK<>XoS5pt;Z2$mxoaDQASW{WoH@r?rD4|FRML-Qz5_&I|3B4nPgf3DQA|QeY zVwponnp71<4G=I1popRr9fOJjHbfX3I)I>v4LdrH^PVts-}lqL=Y8JyyS~4^D_550 zu=hH@z53c`p9~;u09e8XP(i>+P6YsdR*)qCtWJs9Xh(N)A!j5eB&FJ6zt(8RZQGV? zXKb9Fo^F(GZnPz3ld-9-t*x<%nX#FfAvVG=bw|>+$PB}zR4vW5Ai|CDAw|GTGSF$i!I42ODJPm=YVg zZA%Jc%a(+-_QFwQmxRdFII?fbmW{EgsTljn1hS*Q71@v+l@^~6L$w{F}Jj_#{RWjl>NV-`VVeSTcXp1 za{HTG%>Mzme>3$T+}5SUZ;HoE_P^k2Zf0w1YHMaHm=h z`JamUmz@4X|GcOui3Lam{G(%&V#%fef65mH2`}U#vW)n|*i^Dl zY&w~~B{4DyQehV6z#kDHrZ}Pq1)oUZ5OAUqqBUaY#g>Zei${p3iOWeOO1zcmBEBYO zN}5VVNV!VClMa@7AwyiEzr<^al5B(AGIW zwUcV$>N?9v%d?hSui&ppBZp}qnm(GFHBrqYnin*0Yffw4)!MARpxvQkqx(?TSnr2k zF~ySlh3cnYY~W;g$FRvL-1wdG2@_@0lcpMGznJ}OZefvcVQgt{IcMo>MYq~&^}y<- z)gM-&*4@@hHn(i@ZP(aQR{gQsZq3-5(x3Q04gAESZL*gIS?Lq9`|f4uewfactp4sf z4%RbdT)(2XghP#koq!AXCSSH5h~&}W zSrCz_*nusKX@#;)aUx%VD%f;mO;1wiQ{16*6vVXYV1URx z8YS=lkPE)!AR!;@;8X!im^3hXym0Es?*m;)5^mVd3cqWu(6|$pc83~v+Pwa5Z_$wd z`u#9dx2))w_v&fa!b*O>UFP<5yG6^uj=*Ip=5N>PXq66+*0ts|nD%~>Ykj=;^UTI0 zH4z`C-rI~E+9Yb5tur0B^6ntED@ zx(Qqu+O(4ER~|gmHZPSWDp`2{ynXW!S&b6T zB#{V?$vy+QA<*r1yOFp4@xs7XqF;UkCD5TVYb#1XIS_E;Qv(0FbjOjG1Me7x*uq{P z)PAx1L0s~ML5&N|2lqJ-Jju#CI zn$=jI*51T(544FT!v;$lvg@)&C8<_I>ZDl>^}4np8ziqud*O~FAoX!S^*JsG#87N$ zUDlGU3dY`Idjs}}p+W?%N&v*B1@3P11<;ye!(Jby+j9ouL^2>d-M!9^wno6IGDYhA zq{_x_-(OTGfNV*F$H8G~HL+XogbKjl9VG-^8y$VM`)IRb*yI5~GMPa5AeHY;kK~MW z3^Dj6oRa{?@oSE0*3rOHUcF^s(L5~AWW$!p8wM;z*RK;mu|?s`dVWEUJBo9pf;e|d zTE5{dS|1^_>N$%OA9l&cO@{HR?A6Rk#O`G84*OF6pr}=^a}4)MIqjrqWlhG${U*>9 z3?hAvyw$3*G4v$;%4LDB21*-ApMnqi-`fSG8v?}0%sF`dwfm+!Yl0ngBW3R~b41)z z1PJ3Ah^ocp1tnGjvZ)3LjcS(M0IS4Cot*Hot;~hYvuFLsT4dq$7~pv;H?C!59xAM( zA#{I0(v>Wo#{*G7#{efEpHuF1|nW0kZa<3`BjGdTOYaX ziu9Tedf9q#z8;Yy705gZE<12JfsGxutm#It5=I$4} zvl2=DahT5xm3t=$&I?2~D%}pQ31A0=`feJ1@N5?>$NV#|KPHx`SW%;04)h}^&Szqj z<0JLGIRca6Wu?W)V8eVG5CiH>ZqSZb*y5WVdP^2@%mtmju@dr5uv!tIJAG;_{jKI? zmo|z`yOFShIT^;UZxa{1!t)POLf2^6{gN4J1Ik^kpfGJbmfd1JhAPh9YK|GA;*U6W4j#1Wf<8+%(zY_4gOC-MP3oz~zsX9^_u|o$0 zz553Kqz1>HF}c>!8d<;v9H|9F#4N~c;zy}tn+@g6^UHYVoi+o8J&j6d?eoj#^4{A5 zPN4+|FE1ok63?q1w_9bx8?GV_t(!~W{6K4jQFr>TeqH?UimM8lMQJexplVl+>=FC* zJ$wHAeiWB`A8gC#WMXV!Hzs7sJ!bs$D)K^6L$x=z^r}=7!>6HUm`gvS;|uj3AQOEk zCwUwcSU?y$>&AZ>so9pL+59uH6>m+k$J?fh4^_@u6XxmMuxd7v&jwF3$CQ$|@lGHb z(r^c_^X`oI4AEJsR!XNIc$Djvb5`Lbx2+sxD{MGark&?<#ztu_nlx^_wynV|l7gKzM zF3Is?9msd%AwBJ7XXdISy#PN^hdA8b(A?A91C5oS_f#`8l9db%N?`ahvpDk!kTI_g z-o@n^h3u69B7+U~67p5w%TSTCT#!}sDV=LGv6gthVgh5{cPzki-uHUbj`RArUevR( zD}1=Tu^rNi*eNo^B=B9u5r^Al^Zs+-=Hi7Tx_@>5ITU z)@-m68thb%-m;yitE|R}f&A-y6j_FpyXYxZ z8_iNXMa=5$LvgoPN5^q7nLoEpDVr1Awc=YC^1lP+jsSO7A!!w}8sq&!zvubRcNCX! z*@)yjW9O)ed2E5Rr#%9Q!59#BZVHHG9tqCdjv`(vpuk(nti$geE*$G~WvuWk$yRZu zi$p4j0l{k;#9MP~G^6AJH_QNW=_}&W0cB>KOExvzU6^9A_{jT@J<2CQ^yw?T&Ao)3 zvi0G8