/*
* Copyright (C) 2010-2013 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;
import SevenZip.Compression.LZMA.Encoder;
import com.jpexs.decompiler.flash.abc.ABC;
import com.jpexs.decompiler.flash.abc.ClassPath;
import com.jpexs.decompiler.flash.abc.RenameType;
import com.jpexs.decompiler.flash.abc.ScriptPack;
import com.jpexs.decompiler.flash.action.Action;
import com.jpexs.decompiler.flash.action.ActionGraphSource;
import com.jpexs.decompiler.flash.action.swf4.ActionEquals;
import com.jpexs.decompiler.flash.action.swf4.ActionGetVariable;
import com.jpexs.decompiler.flash.action.swf4.ActionIf;
import com.jpexs.decompiler.flash.action.swf4.ActionPush;
import com.jpexs.decompiler.flash.action.swf4.ActionSetVariable;
import com.jpexs.decompiler.flash.action.swf4.ConstantIndex;
import com.jpexs.decompiler.flash.action.swf5.ActionCallFunction;
import com.jpexs.decompiler.flash.action.swf5.ActionCallMethod;
import com.jpexs.decompiler.flash.action.swf5.ActionConstantPool;
import com.jpexs.decompiler.flash.action.swf5.ActionDefineFunction;
import com.jpexs.decompiler.flash.action.swf5.ActionDefineLocal;
import com.jpexs.decompiler.flash.action.swf5.ActionDefineLocal2;
import com.jpexs.decompiler.flash.action.swf5.ActionEquals2;
import com.jpexs.decompiler.flash.action.swf5.ActionGetMember;
import com.jpexs.decompiler.flash.action.swf5.ActionNewMethod;
import com.jpexs.decompiler.flash.action.swf5.ActionNewObject;
import com.jpexs.decompiler.flash.action.swf5.ActionSetMember;
import com.jpexs.decompiler.flash.action.swf7.ActionDefineFunction2;
import com.jpexs.decompiler.flash.action.treemodel.ConstantPool;
import com.jpexs.decompiler.flash.action.treemodel.DirectValueTreeItem;
import com.jpexs.decompiler.flash.action.treemodel.FunctionTreeItem;
import com.jpexs.decompiler.flash.action.treemodel.GetMemberTreeItem;
import com.jpexs.decompiler.flash.action.treemodel.GetVariableTreeItem;
import com.jpexs.decompiler.flash.action.treemodel.clauses.ClassTreeItem;
import com.jpexs.decompiler.flash.action.treemodel.clauses.InterfaceTreeItem;
import com.jpexs.decompiler.flash.ecma.Null;
import com.jpexs.decompiler.flash.flv.AUDIODATA;
import com.jpexs.decompiler.flash.flv.FLVOutputStream;
import com.jpexs.decompiler.flash.flv.FLVTAG;
import com.jpexs.decompiler.flash.flv.VIDEODATA;
import com.jpexs.decompiler.flash.graph.GraphSourceItem;
import com.jpexs.decompiler.flash.graph.GraphSourceItemContainer;
import com.jpexs.decompiler.flash.graph.GraphTargetItem;
import com.jpexs.decompiler.flash.helpers.Cache;
import com.jpexs.decompiler.flash.helpers.Helper;
import com.jpexs.decompiler.flash.tags.ABCContainerTag;
import com.jpexs.decompiler.flash.tags.DefineBinaryDataTag;
import com.jpexs.decompiler.flash.tags.DefineButton2Tag;
import com.jpexs.decompiler.flash.tags.DefineButtonTag;
import com.jpexs.decompiler.flash.tags.DefineSoundTag;
import com.jpexs.decompiler.flash.tags.DefineSpriteTag;
import com.jpexs.decompiler.flash.tags.DefineVideoStreamTag;
import com.jpexs.decompiler.flash.tags.DoInitActionTag;
import com.jpexs.decompiler.flash.tags.ExportAssetsTag;
import com.jpexs.decompiler.flash.tags.FileAttributesTag;
import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag;
import com.jpexs.decompiler.flash.tags.ShowFrameTag;
import com.jpexs.decompiler.flash.tags.SoundStreamBlockTag;
import com.jpexs.decompiler.flash.tags.SymbolClassTag;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.VideoFrameTag;
import com.jpexs.decompiler.flash.tags.base.ASMSource;
import com.jpexs.decompiler.flash.tags.base.BoundedTag;
import com.jpexs.decompiler.flash.tags.base.CharacterIdTag;
import com.jpexs.decompiler.flash.tags.base.CharacterTag;
import com.jpexs.decompiler.flash.tags.base.Container;
import com.jpexs.decompiler.flash.tags.base.DrawableTag;
import com.jpexs.decompiler.flash.tags.base.ImageTag;
import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag;
import com.jpexs.decompiler.flash.tags.base.RemoveTag;
import com.jpexs.decompiler.flash.tags.base.ShapeTag;
import com.jpexs.decompiler.flash.tags.base.SoundStreamHeadTypeTag;
import com.jpexs.decompiler.flash.tags.base.TextTag;
import com.jpexs.decompiler.flash.types.CXFORM;
import com.jpexs.decompiler.flash.types.CXFORMWITHALPHA;
import com.jpexs.decompiler.flash.types.MATRIX;
import com.jpexs.decompiler.flash.types.RECT;
import com.jpexs.decompiler.flash.types.filters.BlendComposite;
import com.jpexs.decompiler.flash.types.filters.FILTER;
import com.jpexs.decompiler.flash.types.filters.Filtering;
import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD;
import com.jpexs.decompiler.flash.types.sound.AdpcmDecoder;
import com.jpexs.decompiler.flash.xfl.XFLConverter;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import javax.imageio.ImageIO;
/**
* Class representing SWF file
*
* @author JPEXS
*/
public class SWF {
/**
* Default version of SWF file format
*/
public static final int DEFAULT_VERSION = 10;
/**
* Tags inside of file
*/
public List tags = new ArrayList<>();
/**
* Rectangle for the display
*/
public RECT displayRect;
/**
* Movie frame rate
*/
public int frameRate;
/**
* Number of frames in movie
*/
public int frameCount;
/**
* Version of SWF
*/
public int version;
/**
* Size of the file
*/
public long fileSize;
/**
* Use compression
*/
public boolean compressed = false;
/**
* Use LZMA compression
*/
public boolean lzma = false;
/**
* Compressed size of the file (LZMA)
*/
public long compressedSize;
/**
* LZMA Properties
*/
public byte lzmaProperties[];
public FileAttributesTag fileAttributes;
/**
* Gets all tags with specified id
*
* @param tagId Identificator of tag type
* @return List of tags
*/
public List getTagData(int tagId) {
List ret = new ArrayList<>();
for (Tag tag : tags) {
if (tag.getId() == tagId) {
ret.add(tag);
}
}
return ret;
}
/**
* Saves this SWF into new file
*
* @param os OutputStream to save SWF in
* @throws IOException
*/
public void saveTo(OutputStream os) throws IOException {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
SWFOutputStream sos = new SWFOutputStream(baos, version);
sos.writeRECT(displayRect);
sos.writeUI8(0);
sos.writeUI8(frameRate);
sos.writeUI16(frameCount);
sos.writeTags(tags);
sos.writeUI16(0);
sos.close();
if (compressed && lzma) {
os.write('Z');
} else if (compressed) {
os.write('C');
} else {
os.write('F');
}
os.write('W');
os.write('S');
os.write(version);
byte data[] = baos.toByteArray();
sos = new SWFOutputStream(os, version);
sos.writeUI32(data.length + 8);
if (compressed) {
if (lzma) {
Encoder enc = new Encoder();
int val = lzmaProperties[0] & 0xFF;
int lc = val % 9;
int remainder = val / 9;
int lp = remainder % 5;
int pb = remainder / 5;
int dictionarySize = 0;
for (int i = 0; i < 4; i++) {
dictionarySize += ((int) (lzmaProperties[1 + i]) & 0xFF) << (i * 8);
}
enc.SetDictionarySize(dictionarySize);
enc.SetLcLpPb(lc, lp, pb);
baos = new ByteArrayOutputStream();
enc.SetEndMarkerMode(true);
enc.Code(new ByteArrayInputStream(data), baos, -1, -1, null);
data = baos.toByteArray();
byte udata[] = new byte[4];
udata[0] = (byte) (data.length & 0xFF);
udata[1] = (byte) ((data.length >> 8) & 0xFF);
udata[2] = (byte) ((data.length >> 16) & 0xFF);
udata[3] = (byte) ((data.length >> 24) & 0xFF);
os.write(udata);
os.write(lzmaProperties);
} else {
os = new DeflaterOutputStream(os);
}
}
os.write(data);
} finally {
if (os != null) {
os.close();
}
}
}
public SWF(InputStream is, boolean paralelRead) throws IOException {
this(is, null, paralelRead);
}
/**
* Construct SWF from stream
*
* @param is Stream to read SWF from
* @param listener
* @param paralelRead Use parallel threads?
* @throws IOException
*/
public SWF(InputStream is, PercentListener listener, boolean paralelRead) throws IOException {
byte hdr[] = new byte[3];
is.read(hdr);
String shdr = new String(hdr, "utf-8");
if ((!shdr.equals("FWS")) && (!shdr.equals("CWS")) && (!shdr.equals("ZWS"))) {
throw new IOException("Invalid SWF file");
}
version = is.read();
SWFInputStream sis = new SWFInputStream(is, version, 4);
fileSize = sis.readUI32();
if (hdr[0] == 'C') {
sis = new SWFInputStream(new InflaterInputStream(is), version, 8);
compressed = true;
}
if (hdr[0] == 'Z') {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sis.readUI32(); //outSize
int propertiesSize = 5;
lzmaProperties = new byte[propertiesSize];
if (sis.read(lzmaProperties, 0, propertiesSize) != propertiesSize) {
throw new IOException("LZMA:input .lzma file is too short");
}
SevenZip.Compression.LZMA.Decoder decoder = new SevenZip.Compression.LZMA.Decoder();
if (!decoder.SetDecoderProperties(lzmaProperties)) {
throw new IOException("LZMA:Incorrect stream properties");
}
if (!decoder.Code(sis, baos, fileSize - 8)) {
throw new IOException("LZMA:Error in data stream");
}
sis = new SWFInputStream(new ByteArrayInputStream(baos.toByteArray()), version, 8);
compressed = true;
lzma = true;
}
if (listener != null) {
sis.addPercentListener(listener);
}
sis.setPercentMax(fileSize);
displayRect = sis.readRECT();
// FIXED8 (16 bit fixed point) frameRate
sis.readUI8(); //tmpFirstByetOfFrameRate
frameRate = sis.readUI8();
frameCount = sis.readUI16();
tags = sis.readTagList(0, paralelRead, true);
assignExportNamesToSymbols();
assignClassesToSymbols();
findFileAttributes();
}
private void findFileAttributes() {
for (Tag t : tags) {
if (t instanceof FileAttributesTag) {
fileAttributes = (FileAttributesTag) t;
break;
}
}
}
private void assignExportNamesToSymbols() {
HashMap exportNames = new HashMap<>();
for (Tag t : tags) {
if (t instanceof ExportAssetsTag) {
ExportAssetsTag eat = (ExportAssetsTag) t;
for (int i = 0; i < eat.tags.size(); i++) {
if ((!exportNames.containsKey(eat.tags.get(i))) && (!exportNames.containsValue(eat.names.get(i)))) {
exportNames.put(eat.tags.get(i), eat.names.get(i));
}
}
}
}
for (Tag t : tags) {
if (t instanceof CharacterIdTag) {
CharacterIdTag ct = (CharacterIdTag) t;
if (exportNames.containsKey(ct.getCharacterId())) {
ct.setExportName(exportNames.get(ct.getCharacterId()));
}
}
}
}
public void assignClassesToSymbols() {
HashMap classes = new HashMap<>();
for (Tag t : tags) {
if (t instanceof SymbolClassTag) {
SymbolClassTag sct = (SymbolClassTag) t;
for (int i = 0; i < sct.tagIDs.length; i++) {
if ((!classes.containsKey(sct.tagIDs[i])) && (!classes.containsValue(sct.classNames[i]))) {
classes.put(sct.tagIDs[i], sct.classNames[i]);
}
}
}
}
for (Tag t : tags) {
if (t instanceof CharacterIdTag) {
CharacterIdTag ct = (CharacterIdTag) t;
if (classes.containsKey(ct.getCharacterId())) {
ct.setClassName(classes.get(ct.getCharacterId()));
}
}
}
}
/**
* Compress SWF file
*
* @param fis Input stream
* @param fos Output stream
* @return True on success
*/
public static boolean fws2cws(InputStream fis, OutputStream fos) {
try {
byte swfHead[] = new byte[8];
fis.read(swfHead);
if (swfHead[0] != 'F') {
fis.close();
return false;
}
swfHead[0] = 'C';
fos.write(swfHead);
fos = new DeflaterOutputStream(fos);
int i;
while ((i = fis.read()) != -1) {
fos.write(i);
}
fis.close();
fos.close();
} catch (FileNotFoundException ex) {
return false;
} catch (IOException ex) {
return false;
}
return true;
}
public static boolean decompress(InputStream fis, OutputStream fos) {
try {
byte hdr[] = new byte[3];
fis.read(hdr);
String shdr = new String(hdr, "utf-8");
if (shdr.equals("CWS")) {
int version = fis.read();
SWFInputStream sis = new SWFInputStream(fis, version, 4);
long fileSize = sis.readUI32();
SWFOutputStream sos = new SWFOutputStream(fos, version);
sos.write("FWS".getBytes("utf-8"));
sos.writeUI8(version);
sos.writeUI32(fileSize);
InflaterInputStream iis = new InflaterInputStream(fis);
int i;
while ((i = iis.read()) != -1) {
fos.write(i);
}
fis.close();
fos.close();
} else if (shdr.equals("ZWS")) {
int version = fis.read();
SWFInputStream sis = new SWFInputStream(fis, version, 4);
long fileSize = sis.readUI32();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sis.readUI32(); //outSize
int propertiesSize = 5;
byte lzmaProperties[] = new byte[propertiesSize];
if (sis.read(lzmaProperties, 0, propertiesSize) != propertiesSize) {
throw new IOException("LZMA:input .lzma file is too short");
}
SevenZip.Compression.LZMA.Decoder decoder = new SevenZip.Compression.LZMA.Decoder();
if (!decoder.SetDecoderProperties(lzmaProperties)) {
throw new IOException("LZMA:Incorrect stream properties");
}
if (!decoder.Code(sis, baos, fileSize - 8)) {
throw new IOException("LZMA:Error in data stream");
}
try (SWFOutputStream sos = new SWFOutputStream(fos, version)) {
sos.write("FWS".getBytes("utf-8"));
sos.write(version);
sos.writeUI32(fileSize);
sos.write(baos.toByteArray());
}
fis.close();
fos.close();
} else {
return false;
}
} catch (FileNotFoundException ex) {
return false;
} catch (IOException ex) {
return false;
}
return true;
}
public boolean exportAS3Class(String className, String outdir, boolean isPcode, boolean paralel) throws Exception {
List abcTags = new ArrayList<>();
for (Tag t : tags) {
if (t instanceof ABCContainerTag) {
ABCContainerTag cnt = (ABCContainerTag) t;
abcTags.add(cnt);
}
}
for (int i = 0; i < abcTags.size(); i++) {
ABC abc = abcTags.get(i).getABC();
ScriptPack scr = abc.findScriptTraitByPath(className);
if (scr != null) {
String cnt = "";
if (abc.script_info.length > 1) {
cnt = "script " + (i + 1) + "/" + abc.script_info.length + " ";
}
String exStr = "Exporting " + "tag " + (i + 1) + "/" + abcTags.size() + " " + cnt + scr.getPath() + " ...";
informListeners("export", exStr);
scr.export(outdir, abcTags, isPcode, paralel);
return true;
}
}
return false;
}
private List> uniqueAS3Packs(List> packs) {
List> ret = new ArrayList<>();
for (KeyValue item : packs) {
for (KeyValue itemOld : ret) {
if (item.key.equals(itemOld.key)) {
Logger.getLogger(SWF.class.getName()).log(Level.SEVERE, "Duplicate pack path found!");
break;
}
}
ret.add(item);
}
return ret;
}
public List> getAS3Packs() {
List abcTags = new ArrayList<>();
for (Tag t : tags) {
if (t instanceof ABCContainerTag) {
abcTags.add((ABCContainerTag) t);
}
}
List> packs = new ArrayList<>();
for (int i = 0; i < abcTags.size(); i++) {
ABCContainerTag t = abcTags.get(i);
packs.addAll(t.getABC().getScriptPacks());
}
return uniqueAS3Packs(packs);
}
private class ExportPackTask implements Callable {
ScriptPack pack;
String directory;
List abcList;
boolean pcode;
String informStr;
ClassPath path;
AtomicInteger index;
int count;
boolean paralel;
public ExportPackTask(AtomicInteger index, int count, ClassPath path, ScriptPack pack, String directory, List abcList, boolean pcode, String informStr, boolean paralel) {
this.pack = pack;
this.directory = directory;
this.abcList = abcList;
this.pcode = pcode;
this.informStr = informStr;
this.path = path;
this.index = index;
this.count = count;
this.paralel = paralel;
}
@Override
public File call() throws Exception {
try {
return pack.export(directory, abcList, pcode, paralel);
} catch (IOException ex) {
Logger.getLogger(ABC.class.getName()).log(Level.SEVERE, null, ex);
}
synchronized (ABC.class) {
informListeners("export", "Exported " + informStr + " script " + index.getAndIncrement() + "/" + count + " " + path);
}
return null;
}
}
public List exportActionScript2(String outdir, boolean isPcode, boolean paralel, EventListener evl) {
List ret = new ArrayList<>();
List