package com.jpexs.decompiler.flash.importers.amf.amf3; import com.jpexs.decompiler.flash.amf.amf3.ListMap; import com.jpexs.decompiler.flash.amf.amf3.Traits; import com.jpexs.decompiler.flash.amf.amf3.types.ArrayType; import com.jpexs.decompiler.flash.amf.amf3.types.BasicType; import com.jpexs.decompiler.flash.amf.amf3.types.ByteArrayType; import com.jpexs.decompiler.flash.amf.amf3.types.DateType; import com.jpexs.decompiler.flash.amf.amf3.types.DictionaryType; import com.jpexs.decompiler.flash.amf.amf3.types.ObjectType; import com.jpexs.decompiler.flash.amf.amf3.types.VectorDoubleType; import com.jpexs.decompiler.flash.amf.amf3.types.VectorIntType; import com.jpexs.decompiler.flash.amf.amf3.types.VectorObjectType; import com.jpexs.decompiler.flash.amf.amf3.types.VectorUIntType; import com.jpexs.decompiler.flash.amf.amf3.types.XmlDocType; import com.jpexs.decompiler.flash.amf.amf3.types.XmlType; 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.List; import java.util.Map; import java.util.Set; public class Amf3Importer { private Amf3Lexer lexer; 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<String, Object> objectTable) throws IOException, Amf3ParseException { expectedType(SymbolType.BRACKET_OPEN); List<Object> 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<Object> values = new ArrayList<>(); public JsArray() { } public JsArray(List<Object> values) { this.values = values; } public void add(Object value) { values.add(value); } public List<Object> 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<Object, Object> values = new ListMap<>(); public Object remove(Object key) { return values.remove(key); } public Set<Object> 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 JsArray getJsArray(Object key) throws Amf3ParseException { return (JsArray) getRequired(key, "JsArray"); } public List<Object> getJsArrayOfObject(Object key) throws Amf3ParseException { return getJsArray(key).getValues(); } @SuppressWarnings("unchecked") public List<String> getJsArrayOfString(Object key) throws Amf3ParseException { return (List<String>) getJsArray(key, "String"); } @SuppressWarnings("unchecked") public List<Long> getJsArrayOfInt(Object key) throws Amf3ParseException { return (List<Long>) getJsArray(key, "int"); } @SuppressWarnings("unchecked") public List<Long> getJsArrayOfUint(Object key) throws Amf3ParseException { return (List<Long>) getJsArray(key, "uint"); } @SuppressWarnings("unchecked") public List<Double> getJsArrayOfNumber(Object key) throws Amf3ParseException { return (List<Double>) getJsArray(key, "Number"); } public List getJsArray(Object key, String valueType) throws Amf3ParseException { JsArray jsArr = (JsArray) getRequired(key, "JsArray"); switch (valueType) { case "String": List<String> 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<Long> 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<Double> 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 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<String, Object> objectTable, boolean allowTypedObject) throws Amf3ParseException { Object val = values.get(key); Object resolved = resolveObjects(val, objectTable, allowTypedObject); values.put(key, resolved); } public List<String> stringKeys() { List<String> ret = new ArrayList<>(); for (Object key : values.keySet()) { if (key instanceof String) { ret.add((String) key); } } return ret; } public Map<Object, Object> getAll() { return values; } public Map<String, Object> getStringMapped() { Map<String, Object> ret = new ListMap<>(); 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<String, Object> 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<String, Object> 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"); try { resultObject = new DateType((double) sdf.parse(dateStr).getTime()); } catch (ParseException ex) { throw new Amf3ParseException("Invalid date format: " + dateStr, lexer.yyline()); } break; case "XML": resultObject = new XmlType(typedObject.getString("value")); break; case "XMLDocument": resultObject = new XmlDocType(typedObject.getString("value")); break; case "Object": String className = typedObject.getString("className"); if (typedObject.containsKey("serialized")) { //TODO } else { boolean dynamic = typedObject.getBoolean("dynamic"); typedObject.resolve("sealedMembers", objectTable, false); JsObject jsoSealed = typedObject.getJsObject("sealedMembers"); Map<String, Object> sealedMembers = jsoSealed.getStringMapped(); typedObject.resolve("dynamicMembers", objectTable, false); Map<String, Object> dynamicMembers = typedObject.getJsObject("dynamicMembers").getStringMapped(); List<String> sealedMemberNames = new ArrayList<>(jsoSealed.stringKeys()); resultObject = new ObjectType(new Traits(className, dynamic, sealedMemberNames), sealedMembers, dynamicMembers); } break; case "Array": typedObject.resolve("denseValues", objectTable, false); List<Object> denseValues = typedObject.getJsArray("denseValues").getValues(); typedObject.resolve("associativeValues", objectTable, false); JsObject resolvedArr = typedObject.getJsObject("associativeValues"); Map<String, Object> associativeValues = resolvedArr.getStringMapped(); resultObject = new ArrayType(denseValues, associativeValues); break; case "Vector": boolean fixed = typedObject.getBoolean("fixed"); String subtype = typedObject.getString("subtype"); typedObject.resolve("values", objectTable, false); switch (subtype) { case "int": resultObject = new VectorIntType(fixed, typedObject.getJsArrayOfInt("values")); break; case "uint": resultObject = new VectorUIntType(fixed, typedObject.getJsArrayOfUint("values")); break; case "Number": resultObject = new VectorDoubleType(fixed, typedObject.getJsArrayOfNumber("values")); break; default: resultObject = new VectorObjectType(fixed, subtype, typedObject.getJsArrayOfObject("values")); break; } break; case "ByteArray": try { resultObject = new ByteArrayType(javax.xml.bind.DatatypeConverter.parseHexBinary(typedObject.getString("value"))); } catch (IllegalArgumentException iex) { throw new Amf3ParseException("Invalid hex byte sequence", lexer.yyline()); } break; case "Dictionary": boolean weakKeys = typedObject.getBoolean("weakKeys"); typedObject.resolve("entries", objectTable, false); Map<Object, Object> entries = typedObject.getJsObject("entries").getAll(); resultObject = new DictionaryType(weakKeys, entries); 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(resKey, resVal); } resultObject = jsObject; } } return resultObject; } private Object value(Map<String, Object> 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: case INTEGER: return 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 * @param objectsTable * @return */ private Object replaceReferences(Object object, Map<String, Object> 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.sealedMembersKeySet()) { ot.putSealedMember(key, replaceReferences(ot.getSealedMember(key), objectsTable)); } for (String key : ot.dynamicMembersKeySet()) { ot.putDynamicMember(key, replaceReferences(ot.getDynamicMember(key), objectsTable)); } for (String key : ot.serializedMembersKeySet()) { ot.putSerializedMember(key, replaceReferences(ot.getSerializedMember(key), objectsTable)); } } else if (object instanceof ArrayType) { ArrayType at = (ArrayType) object; for (String key : at.associativeKeySet()) { at.putAssociative(key, replaceReferences(at.getAssociative(key), objectsTable)); } for (int i = 0; i < at.getDenseValues().size(); i++) { at.setDense(i, replaceReferences(at.getDense(i), objectsTable)); } } else if (object instanceof VectorObjectType) { VectorObjectType vot = (VectorObjectType) object; for (int i = 0; i < vot.getValues().size(); i++) { vot.getValues().set(i, replaceReferences(vot.getValues().get(i), objectsTable)); } } else if (object instanceof DictionaryType) { DictionaryType dt = (DictionaryType) object; for (Object key : dt.keySet()) { Object val = dt.get(key); Object newKey = replaceReferences(key, objectsTable); Object newVal = replaceReferences(val, objectsTable); dt.remove(key); dt.put(newKey, newVal); } } 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; } } public Object stringToAmf(String val) throws IOException, Amf3ParseException { lexer = new Amf3Lexer(new StringReader(val)); Map<String, Object> objectsTable = new HashMap<>(); List<ReferencedObjectType> references = new ArrayList<>(); Object result = value(objectsTable); Object resultResolved = resolveObjects(result, objectsTable, true); Object resultNoRef = replaceReferences(resultResolved, objectsTable); return resultNoRef; } }