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 000000000..8cbdf0426 Binary files /dev/null and b/libsrc/ffdec_lib/testdata/sharedobjects/data/amf0test.sol differ 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 000000000..d43561122 Binary files /dev/null and b/libsrc/ffdec_lib/testdata/sharedobjects/data/amf3test.sol differ diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/data/bad.sol b/libsrc/ffdec_lib/testdata/sharedobjects/data/bad.sol new file mode 100644 index 000000000..d6459e005 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/data/bad.sol @@ -0,0 +1 @@ +xxx diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.html b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.html new file mode 100644 index 000000000..0e035e62e --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.html @@ -0,0 +1,49 @@ + + + + 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 000000000..065c94ae0 Binary files /dev/null and b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects.swf differ diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/DOMDocument.xml b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/DOMDocument.xml new file mode 100644 index 000000000..e9493ef95 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/DOMDocument.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/LoadButton.xml b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/LoadButton.xml new file mode 100644 index 000000000..6461b8c31 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/LoadButton.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + Load + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/SaveButton.xml b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/SaveButton.xml new file mode 100644 index 000000000..1b865e79d --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/LIBRARY/SaveButton.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + Save + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/META-INF/metadata.xml b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/META-INF/metadata.xml new file mode 100644 index 000000000..99d455357 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/META-INF/metadata.xml @@ -0,0 +1,55 @@ + + + + + Adobe Flash Professional CS6 - build 481 + 2024-11-03T09:46:34-08:00 + 2024-11-03T09:51:04-08:00 + 2024-11-03T09:51:04-08:00 + + + application/vnd.adobe.fla + + + xmp.iid:329919207399EF119BABD30F3587D305 + xmp.did:329919207399EF119BABD30F3587D305 + xmp.did:329919207399EF119BABD30F3587D305 + + + + created + xmp.iid:329919207399EF119BABD30F3587D305 + 2024-11-03T09:46:34-08:00 + Adobe Flash Professional CS6 - build 481 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/MobileSettings.xml b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/MobileSettings.xml new file mode 100644 index 000000000..e69de29bb diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/PublishSettings.xml b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/PublishSettings.xml new file mode 100644 index 000000000..ee81b1d10 --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/PublishSettings.xml @@ -0,0 +1,206 @@ + + + + 1 + 1 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + sharedobjects.swf + sharedobjects.exe + sharedobjects.app + sharedobjects.html + sharedobjects.gif + sharedobjects.jpg + sharedobjects.png + sharedobjects.mov + sharedobjects.smil + sharedobjects.swc + + + 0 + 12,0,0,0;11,2,0,0;11,1,0,0;10,3,0,0;10,2,153,0;10,1,52,0;9,0,124,0;8,0,24,0;7,0,14,0;6,0,79,0;5,0,58,0;4,0,32,0;3,0,8,0;2,0,1,12;1,0,0,1; + 1 + 1 + sharedobjects.xfl_content.html + sharedobjects.xfl_alternate.html + 0 + + 550 + 400 + 0 + 0 + 1 + 0 + 0 + 1 + 1 + 4 + 0 + 0 + 1 + 0 + C:\Users\MyUser\AppData\Local\Adobe\Flash CS6\en_US\Configuration\HTML\Default.html + 1 + + + + + 0 + 0 + 0 + 80 + 0 + 0 + 7 + 0 + 7 + 0 + 15 + FlashPlayer11.2 + 3 + 1 + + . + CONFIG::FLASH_AUTHORING="true"; + 0 + + 1 + 0 + 1 + 0 + 0 + 0 + 0 + Main + 2 + 4 + 4096 + AS3 + 1 + 1 + 0 + 15 + 1 + 0 + 4102 + rsl + wrap + $(AppConfig)/ActionScript 3.0/rsls/loader_animation.swf + + + $(AppConfig)/ActionScript 3.0/libs + merge + + + $(AppConfig)/ActionScript 3.0/libs/11.0/textLayout.swc + rsl + http://fpdownload.adobe.com/pub/swz/tlf/2.0.0.232/textLayout_2.0.0.232.swz + http://fpdownload.adobe.com/pub/swz/crossdomain.xml + textLayout_2.0.0.232.swz + + + + + $(AppConfig)/ActionScript 3.0/libs/11.0/textLayout.swc + + http://fpdownload.adobe.com/pub/swz/tlf/2.0.0.232/textLayout_2.0.0.232.swz + http://fpdownload.adobe.com/pub/swz/crossdomain.xml + textLayout_2.0.0.232.swz + + + + + 550 + 400 + 0 + 4718592 + 0 + 80 + 1 + + + 1 + 0 + 1 + 0 + 0 + 100000 + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + + + 550 + 400 + 0 + 1 + 1 + + 1 + 0 + 1 + 0 + 0 + + 128 + + + 255 + + + + 550 + 400 + 1 + 0 + 0 + 1 + 0 + 0 + 1 + + + + 24-bit with Alpha + 255 + + + + 550 + 400 + 1 + 0 + + + 00000000 + 0 + 0 + 0 + 0 + 1 + + + \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/bin/SymDepend.cache b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/bin/SymDepend.cache new file mode 100644 index 000000000..9a6c83f8f Binary files /dev/null and b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/bin/SymDepend.cache differ diff --git a/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/sharedobjects.xfl b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/sharedobjects.xfl new file mode 100644 index 000000000..860a820ec --- /dev/null +++ b/libsrc/ffdec_lib/testdata/sharedobjects/sharedobjects/sharedobjects.xfl @@ -0,0 +1 @@ +PROXY-CS5 \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/Main.java b/src/com/jpexs/decompiler/flash/gui/Main.java index 7ac23ed9f..13cbfb8fd 100644 --- a/src/com/jpexs/decompiler/flash/gui/Main.java +++ b/src/com/jpexs/decompiler/flash/gui/Main.java @@ -50,6 +50,7 @@ import com.jpexs.decompiler.flash.gui.debugger.DebugAdapter; import com.jpexs.decompiler.flash.gui.debugger.DebugLoaderDataModified; import com.jpexs.decompiler.flash.gui.debugger.DebuggerTools; import com.jpexs.decompiler.flash.gui.pipes.FirstInstance; +import com.jpexs.decompiler.flash.gui.soleditor.SolEditorFrame; import com.jpexs.decompiler.flash.helpers.SWFDecompilerPlugin; import com.jpexs.decompiler.flash.tags.DefineBinaryDataTag; import com.jpexs.decompiler.flash.tags.DefineVideoStreamTag; @@ -3424,4 +3425,9 @@ public class Main { } return null; } + + public static void openSolEditor() { + SolEditorFrame solEdit = new SolEditorFrame(); + solEdit.setVisible(true); + } } diff --git a/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java b/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java index cb469d34e..a1ca1952f 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java +++ b/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java @@ -859,6 +859,10 @@ public abstract class MainFrameMenu implements MenuBuilder { } Main.advancedSettings(); } + + protected void solEditorActionPerformed(ActionEvent evt) { + Main.openSolEditor(); + } protected void searchMemoryActionPerformed(ActionEvent evt) { Main.loadFromMemory(); @@ -1332,6 +1336,8 @@ public abstract class MainFrameMenu implements MenuBuilder { addMenuItem("/tools/replace", translate("menu.tools.replace"), "replace32", this::replaceActionPerformed, PRIORITY_TOP, null, true, null, false); addMenuItem("/tools/abcExplorer", translate("menu.tools.abcexplorer"), "abcexplorer32", this::abcExplorerActionPerformed, PRIORITY_TOP, null, true, null, false); + addMenuItem("/tools/gotoDocumentClass", translate("menu.tools.gotoDocumentClass"), "gotomainclass32", this::gotoDocumentClassActionPerformed, PRIORITY_TOP, null, true, null, false); + addMenuItem("/tools/solEditor", translate("menu.tools.solEditor"), "soleditor32", this::solEditorActionPerformed, PRIORITY_TOP, null, true, null, false); if (Platform.isWindows()) { addMenuItem("/tools/searchMemory", translate("menu.tools.searchMemory"), "loadmemory16", this::searchMemoryActionPerformed, PRIORITY_MEDIUM, null, true, null, false); } @@ -1351,7 +1357,6 @@ public abstract class MainFrameMenu implements MenuBuilder { //addMenuItem("/tools/debugger/debuggerInjectLoader", "Inject Loader", "debuggerreplace16", this::debuggerInjectLoader, PRIORITY_MEDIUM, null, true,false); addMenuItem("/tools/debugger/debuggerShowLog", translate("menu.debugger.showlog"), "debuggerlog16", this::debuggerShowLogActionPerformed, PRIORITY_MEDIUM, null, true, null,false); finishMenu("/tools/debugger");*/ - addMenuItem("/tools/gotoDocumentClass", translate("menu.tools.gotoDocumentClass"), "gotomainclass32", this::gotoDocumentClassActionPerformed, PRIORITY_TOP, null, true, null, false); finishMenu("/tools"); //Settings diff --git a/src/com/jpexs/decompiler/flash/gui/graphics/soleditor16.png b/src/com/jpexs/decompiler/flash/gui/graphics/soleditor16.png new file mode 100644 index 000000000..db60f042e Binary files /dev/null and b/src/com/jpexs/decompiler/flash/gui/graphics/soleditor16.png differ diff --git a/src/com/jpexs/decompiler/flash/gui/graphics/soleditor32.png b/src/com/jpexs/decompiler/flash/gui/graphics/soleditor32.png new file mode 100644 index 000000000..11dad92ab Binary files /dev/null and b/src/com/jpexs/decompiler/flash/gui/graphics/soleditor32.png differ diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties index 488e43648..1b7af2b17 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties @@ -547,4 +547,8 @@ config.name.lastSessionEasySwf = Last Simple editor session file config.description.lastSessionEasySwf = Contains the selected SWF from the last session in Simple editor config.name.maxScriptLineLength = Maximum script line length -config.description.maxScriptLineLength = Maximum line length in the script editor before line wrapping occurs. 0 = unlimited. On linux there might be problems on displaying very large lines so that's why it's limited by default. \ No newline at end of file +config.description.maxScriptLineLength = Maximum line length in the script editor before line wrapping occurs. 0 = unlimited. On linux there might be problems on displaying very large lines so that's why it's limited by default. + +#after 21.1.3 +config.name.lastSolEditorDirectory = Last Sol editor directory +config.description.lastSolEditorDirectory = Directory where last SOL file was opened/saved \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 63a15d9a5..c99d59787 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1010,4 +1010,7 @@ contextmenu.convertPlaceObjectType = Convert place object type work.decompiling.allScripts.ucf = Decompiling all scripts to get uninitialized class fields. This may take a while #after 21.1.1 -menu.file.view.easy = Simple editor \ No newline at end of file +menu.file.view.easy = Simple editor + +#after 21.1.3 +menu.tools.solEditor = Sol editor \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/soleditor/SolEditorFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/soleditor/SolEditorFrame.properties new file mode 100644 index 000000000..e33313c56 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/soleditor/SolEditorFrame.properties @@ -0,0 +1,20 @@ +dialog.title = Sol editor + +button.new = New +button.open = Open... +button.save = Save +button.saveAs = Save as... + +filter.sol = SOL files (*.sol) + +error.cannotOpen = Cannot open file. +error.parse = Parse error. %reason% on line %line%. + +info.saved = File successfully saved. + +filename = File name: +amfVersion = AMF version: + +warning.loseChanges = Current file is modified. Do you really want to lose changes? + +untitled = Untitled \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/soleditor/SolEditorFrame.java b/src/com/jpexs/decompiler/flash/gui/soleditor/SolEditorFrame.java new file mode 100644 index 000000000..199224d1e --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/soleditor/SolEditorFrame.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2010-2024 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.gui.soleditor; + +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.amf.amf0.Amf0Exporter; +import com.jpexs.decompiler.flash.exporters.amf.amf3.Amf3Exporter; +import com.jpexs.decompiler.flash.gui.AppFrame; +import com.jpexs.decompiler.flash.gui.AppStrings; +import com.jpexs.decompiler.flash.gui.FasterScrollPane; +import com.jpexs.decompiler.flash.gui.View; +import com.jpexs.decompiler.flash.gui.ViewMessages; +import com.jpexs.decompiler.flash.gui.editor.LineMarkedEditorPane; +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 com.jpexs.decompiler.flash.sol.SolFile; +import com.jpexs.helpers.Helper; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import javax.swing.Box; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.filechooser.FileFilter; + +/** + * + * @author JPEXS + */ +public class SolEditorFrame extends AppFrame { + + private File openedFile = null; + private boolean modified = false; + + private final JButton saveAsButton; + private final JButton saveButton; + private JTextField fileNameField = new JTextField(30); + private JComboBox amfVersionComboBox = new JComboBox<>(); + private JLabel amfVersionLabel = new JLabel(); + + private LineMarkedEditorPane editor = new LineMarkedEditorPane(); + + private final DocumentListener modifiedListener; + + public SolEditorFrame() { + setDefaultCloseOperation(HIDE_ON_CLOSE); + setTitle(translate("dialog.title")); + + modifiedListener = new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + setModified(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + setModified(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + setModified(); + } + + private void setModified() { + if (modified) { + return; + } + modified = true; + updateTitle(); + } + }; + + Container cnt = getContentPane(); + cnt.setLayout(new BorderLayout()); + + JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + + JButton newButton = new JButton(translate("button.new"), View.getIcon("newswf16")); + newButton.addActionListener(this::newActionPerformed); + + JButton openButton = new JButton(translate("button.open"), View.getIcon("open16")); + openButton.addActionListener(this::openActionPerformed); + + saveButton = new JButton(translate("button.save"), View.getIcon("save16")); + saveButton.addActionListener(this::saveActionPerformed); + saveButton.setEnabled(false); + + saveAsButton = new JButton(translate("button.saveAs"), View.getIcon("saveas16")); + saveAsButton.addActionListener(this::saveAsActionPerformed); + + topPanel.add(newButton); + topPanel.add(openButton); + topPanel.add(saveButton); + topPanel.add(saveAsButton); + + fileNameField.setText(translate("untitled")); + fileNameField.getDocument().addDocumentListener(modifiedListener); + DefaultComboBoxModel amfVersionModel = new DefaultComboBoxModel<>(); + amfVersionModel.addElement(0); + amfVersionModel.addElement(3); + amfVersionComboBox.setModel(amfVersionModel); + amfVersionComboBox.setSelectedIndex(1); + amfVersionLabel.setText("" + amfVersionComboBox.getSelectedItem()); + amfVersionComboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + amfVersionLabel.setText(amfVersionComboBox.getSelectedItem().toString()); + } + }); + + amfVersionLabel.setVisible(false); + + JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + bottomPanel.add(new JLabel(translate("filename"))); + bottomPanel.add(fileNameField); + bottomPanel.add(Box.createHorizontalStrut(20)); + bottomPanel.add(new JLabel(translate("amfVersion"))); + bottomPanel.add(amfVersionComboBox); + bottomPanel.add(amfVersionLabel); + + cnt.add(new FasterScrollPane(editor), BorderLayout.CENTER); + cnt.add(topPanel, BorderLayout.NORTH); + cnt.add(bottomPanel, BorderLayout.SOUTH); + editor.setText("{\r\n\r\n}"); + setSize(800, 600); + + View.centerScreen(this); + View.setWindowIcon(this, "soleditor"); + } + + private void newActionPerformed(ActionEvent e) { + if (modified && ViewMessages.showConfirmDialog(this, translate("warning.loseChanges"), AppStrings.translate("message.warning"), JOptionPane.WARNING_MESSAGE) != JOptionPane.OK_OPTION) { + return; + } + openedFile = null; + editor.setText("{\r\n\r\n}"); + modified = false; + amfVersionComboBox.setEnabled(true); + amfVersionComboBox.setSelectedItem(3); + amfVersionLabel.setVisible(false); + amfVersionComboBox.setVisible(true); + updateTitle(); + fileNameField.setText(translate("untitled")); + } + + private void openActionPerformed(ActionEvent e) { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileHidingEnabled(false); + fileChooser.setFileFilter(new FileFilter() { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".sol"); + } + + @Override + public String getDescription() { + return translate("filter.sol"); + } + }); + fileChooser.setCurrentDirectory(new File(Configuration.lastSolEditorDirectory.get())); + if (fileChooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) { + return; + } + File newFile = Helper.fixDialogFile(fileChooser.getSelectedFile()); + + try (FileInputStream fis = new FileInputStream(newFile)) { + SolFile solFile = new SolFile(fis); + Map values = solFile.getAmfValues(); + String newFileName = solFile.getFileName(); + int newAmfVersion = solFile.getAmfVersion(); + switch (newAmfVersion) { + case 0: + editor.setText(Amf0Exporter.amfMapToString(values, 0, "\r\n")); + break; + case 3: + editor.setText(Amf3Exporter.amfMapToString(values, " ", "\r\n", 0)); + break; + default: + throw new IllegalArgumentException("No AMF version found"); + } + fileNameField.setText(newFileName); + amfVersionComboBox.setSelectedItem(newAmfVersion); + amfVersionLabel.setText("" + newAmfVersion); + } catch (IOException | IllegalArgumentException ex) { + ViewMessages.showMessageDialog(this, translate("error.cannotOpen") + " " + ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); + return; + } + modified = false; + openedFile = newFile; + updateTitle(); + Configuration.lastSolEditorDirectory.set(newFile.getParentFile().getAbsolutePath()); + saveButton.setEnabled(true); + saveAsButton.setEnabled(true); + amfVersionComboBox.setEnabled(false); + amfVersionComboBox.setVisible(false); + amfVersionLabel.setVisible(true); + } + + private void updateTitle() { + setTitle(translate("dialog.title") + (openedFile != null ? " - " + (modified ? "*" : "") + openedFile.getAbsolutePath() : "")); + } + + private void saveActionPerformed(ActionEvent e) { + if (openedFile == null) { + saveAsActionPerformed(e); + return; + } + saveAs(openedFile); + } + + private void saveAs(File saveFile) { + try { + String amfText = editor.getText(); + String fileName = fileNameField.getText(); + int amfVersion = (Integer) amfVersionComboBox.getSelectedItem(); + Map amfValues; + switch (amfVersion) { + case 0: + Amf0Importer amf0Importer = new Amf0Importer(); + amfValues = amf0Importer.stringToAmfMap(amfText); + break; + case 3: + Amf3Importer amf3Importer = new Amf3Importer(); + amfValues = amf3Importer.stringToAmfMap(amfText); + break; + default: + return; //should not happen + } + try (FileOutputStream fos = new FileOutputStream(saveFile)) { + SolFile solFile = new SolFile(fileName, amfVersion, amfValues); + solFile.writeTo(fos); + } + openedFile = saveFile; + Configuration.lastSolEditorDirectory.set(saveFile.getParentFile().getAbsolutePath()); + modified = false; + updateTitle(); + amfVersionComboBox.setEnabled(false); + amfVersionComboBox.setVisible(false); + amfVersionLabel.setVisible(true); + ViewMessages.showMessageDialog(this, translate("info.saved"), AppStrings.translate("message.info"), JOptionPane.INFORMATION_MESSAGE); + } catch (Amf3ParseException ex) { + editor.gotoLine((int) ex.line); + editor.markError(); + ViewMessages.showMessageDialog(this, translate("error.parse").replace("%reason%", ex.text).replace("%line%", "" + ex.line), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); + } catch (IOException ex) { + ViewMessages.showMessageDialog(this, ex.getLocalizedMessage(), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); + } + } + + private void saveAsActionPerformed(ActionEvent e) { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileHidingEnabled(false); + fileChooser.setFileFilter(new FileFilter() { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".sol"); + } + + @Override + public String getDescription() { + return translate("filter.sol"); + } + }); + fileChooser.setCurrentDirectory(new File(Configuration.lastSolEditorDirectory.get())); + if (fileChooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) { + return; + } + + File newFile = Helper.fixDialogFile(fileChooser.getSelectedFile()); + saveAs(newFile); + } + + @Override + public void setVisible(boolean b) { + super.setVisible(b); + if (b) { + editor.setContentType("text/javascript"); + editor.setText("{\r\n\r\n}"); + editor.getDocument().addDocumentListener(modifiedListener); + } + } +}