Push it to gitea!

This commit is contained in:
Michael Wain 2024-12-29 04:31:01 +03:00
commit 132f3c9d16
28 changed files with 875 additions and 0 deletions

39
.gitignore vendored Normal file

@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

57
pom.xml Normal file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alterdekim.game</groupId>
<artifactId>actionScriptDecompiler</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<exec.mainClass>com.alterdekim.flash.decompiler.Main</exec.mainClass>
</properties>
<dependencies>
<dependency>
<groupId>com.github.jponge</groupId>
<artifactId>lzma-java</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.javatuples</groupId>
<artifactId>javatuples</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</project>

@ -0,0 +1,210 @@
package com.alterdekim.flash.decompiler;
import com.alterdekim.flash.decompiler.action.*;
import com.alterdekim.flash.decompiler.item.*;
import com.alterdekim.flash.decompiler.tag.DoAction;
import com.alterdekim.flash.decompiler.tag.ShockwaveTag;
import com.alterdekim.flash.decompiler.tag.TagType;
import com.alterdekim.flash.decompiler.util.ActionField;
import com.alterdekim.flash.decompiler.util.Bits;
import com.alterdekim.flash.decompiler.util.PayloadCompression;
import com.alterdekim.flash.decompiler.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.javatuples.Triplet;
import java.io.*;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import static com.alterdekim.flash.decompiler.util.ByteUtil.bytesToInt;
import static com.alterdekim.flash.decompiler.util.ByteUtil.bytesToIntRev;
import static com.alterdekim.flash.decompiler.util.Compression.decompressDEFLATE;
import static com.alterdekim.flash.decompiler.util.Compression.decompressLZMA;
@Slf4j
public class FlashDecompiler {
private PayloadCompression compression;
public ShockwaveFile loadFromFile( File flashFile) throws IOException {
return processSWF(Files.readAllBytes(flashFile.toPath()));
}
private ShockwaveFile processSWF(byte[] swfBytes) throws IOException {
compression = PayloadCompression.getByTag(swfBytes[0]);
byte[] data = Arrays.copyOfRange(swfBytes, 8, swfBytes.length);
switch (compression) {
case DEFLATE:
data = decompressDEFLATE(data);
break;
case LZMA:
data = decompressLZMA(data);
break;
}
return processPayload(data);
}
private ShockwaveFile processPayload(byte[] payload) {
BitSet bitSet = BitSet.valueOf(payload);
int c = (payload.length * 8);
int i = 0;
int cnt = Bits.convert(bitSet.get(i, i+7), 7, false); // Rect x=[5] [x] [x] [x] [x]
i += 7;
i += cnt * 4;
i = Bits.ceilToByte(i);
float frameRate = Bits.convertP8(bitSet.get(i, i+16));
i += 16;
int frameCount = Bits.convert(bitSet.get(i, i+16), 16, true);
i += 16;
// 16-bit + 16-bit = 32 bits for each header tag.
i = Bits.ceilToByte(i);
List<ShockwaveTag> shockwaveTags = new ArrayList<>();
while( i < c ) {
BitSet tag = bitSet.get(i, i+16);
int v = bytesToInt(new byte[] {(byte) Bits.convert(tag.get(8, 16), 8, true),
(byte) Bits.convert(tag.get(0, 8), 8, true)});
i += 16;
int type = (v & 65472) >> 6; // type
int len = v & 63; // length in bytes
if( len == 63 ) {
len = Bits.convert(bitSet.get(i, i+32), 32, true);
i += 32;
}
TagType tagType = TagType.fromId(type);
if( tagType == TagType.DoAction ) {
int g = i / 8;
shockwaveTags.add(processDoActionTag(Arrays.copyOfRange(payload, g, g+len)));
} else {
log.error("Unknown tag type: {}", type);
}
i += len * 8;
}
return new ShockwaveFile(frameRate, frameCount, shockwaveTags, compression);
}
private DoAction processDoActionTag(byte[] data) {
int i = 0;
DoAction tag = new DoAction();
tag.setActions(new ArrayList<>());
while( i < data.length ) {
switch(ActionField.fromId(data[i])) {
case ConstantPool:
i++;
i = parseActionPool(data, i, tag);
break;
case Push:
i = parseActionPush(data, i, tag);
break;
case InitObject:
tag.getActions().add(new ActionInitObject());
i++;
break;
case DefineLocal:
i++;
tag.getActions().add(new ActionDefineLocal());
break;
case SetMember:
i++;
tag.getActions().add(new ActionSetMember());
break;
case GetVariable:
i++;
tag.getActions().add(new ActionGetVariable());
break;
default:
log.error("Unknown action field type: {}", StringUtils.bytesToHex(new byte[] {data[i]}));
i++;
break;
}
}
return tag;
}
private Triplet<byte[], Integer, Integer> findNull(byte[] data, int i, int u) {
int s = i;
while( data[i] != 0x00 ) {
i++;
u++;
}
return Triplet.with(Arrays.copyOfRange(data, s, i), i, u);
}
private int parseActionPush(byte[] data, int i, DoAction tag) {
i++;
int len_in_bytes = bytesToIntRev(Arrays.copyOfRange(data, i, i+2));
int u = 0;
i += 2;
int index;
ActionPush push = new ActionPush();
push.setItems(new ArrayList<>());
while( u < len_in_bytes ) {
ActionPushType type = ActionPushType.fromType(data[i]);
switch(type) {
case STRING:
i++;
var result = findNull(data, i, u);
push.getItems().add(new StringItem(new String(result.getValue0())));
i = result.getValue1();
u = result.getValue2();
break;
case POOL_INDEX_BIG:
i++;
index = bytesToIntRev(Arrays.copyOfRange(data, i, i+2));
push.getItems().add(new PoolIndexItem(index));
i += 2;
u += 3;
continue;
case POOL_INDEX:
i++;
index = bytesToIntRev(new byte[] {data[i]});
push.getItems().add(new PoolIndexItem(index));
i++;
u+=2;
continue;
case INTEGER:
i++;
push.getItems().add(new IntegerItem(bytesToIntRev(Arrays.copyOfRange(data, i, i+4))));
i += 4;
u += 5;
continue;
case NULL:
push.getItems().add(new NullItem());
i++;
u++;
continue;
default:
i++;
u++;
log.info("parseActionPush(): Unknown action field: {}", data[i]);
}
}
tag.getActions().add(push);
return i;
}
private int parseActionPool(byte[] data, int i, DoAction tag) {
List<String> actionPool = new ArrayList<>();
int bytes_len = bytesToIntRev(Arrays.copyOfRange(data, i, i+2));
i += 2;
int count = bytesToIntRev(Arrays.copyOfRange(data, i, i+2));
i += 2;
int u = 1;
StringBuilder s = new StringBuilder();
while( u <= count ) {
if( data[i] == 0x00 ) {
actionPool.add(s.toString());
s = new StringBuilder();
u++;
i++;
continue;
}
s.append((char) data[i]);
i++;
}
tag.setActionPool(actionPool);
return i;
}
}

@ -0,0 +1,31 @@
package com.alterdekim.flash.decompiler;
import com.alterdekim.flash.decompiler.tag.DoAction;
import com.alterdekim.flash.decompiler.tag.TagType;
import com.alterdekim.flash.decompiler.translator.Flash2Java;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
@Slf4j
public class Main {
public static void main(String[] args) throws Exception {
FlashDecompiler decompiler = new FlashDecompiler();
ShockwaveFile file = decompiler.loadFromFile(new File("catalogs.swf"));
DoAction da = (DoAction) file.getTags()
.stream()
.filter(p -> p.getType() == TagType.DoAction)
.findFirst()
.get();
Flash2Java converter = new Flash2Java();
ObjectMapper objectMapper = new ObjectMapper();
File out = new File("dec.json");
if( out.exists() ) out.delete();
objectMapper.writeValue(out, converter.convert(da));
}
}

@ -0,0 +1,19 @@
package com.alterdekim.flash.decompiler;
import com.alterdekim.flash.decompiler.tag.ShockwaveTag;
import com.alterdekim.flash.decompiler.util.PayloadCompression;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import java.util.List;
@ToString
@Getter
@AllArgsConstructor
public class ShockwaveFile {
private Float frameRate;
private Integer frameCount;
private List<ShockwaveTag> tags;
private PayloadCompression compression;
}

@ -0,0 +1,16 @@
package com.alterdekim.flash.decompiler.action;
import com.alterdekim.flash.decompiler.util.ActionField;
import lombok.NoArgsConstructor;
import lombok.ToString;
@ToString
@NoArgsConstructor
public class ActionDefineLocal extends ByteCodeAction {
@Override
public ActionField getType() {
return ActionField.DefineLocal;
}
}

@ -0,0 +1,15 @@
package com.alterdekim.flash.decompiler.action;
import com.alterdekim.flash.decompiler.util.ActionField;
import lombok.NoArgsConstructor;
import lombok.ToString;
@ToString
@NoArgsConstructor
public class ActionGetVariable extends ByteCodeAction {
@Override
public ActionField getType() {
return ActionField.GetVariable;
}
}

@ -0,0 +1,15 @@
package com.alterdekim.flash.decompiler.action;
import com.alterdekim.flash.decompiler.util.ActionField;
import lombok.NoArgsConstructor;
import lombok.ToString;
@ToString
@NoArgsConstructor
public class ActionInitObject extends ByteCodeAction {
@Override
public ActionField getType() {
return ActionField.InitObject;
}
}

@ -0,0 +1,24 @@
package com.alterdekim.flash.decompiler.action;
import com.alterdekim.flash.decompiler.util.ActionField;
import com.alterdekim.flash.decompiler.item.ByteCodeItem;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@ToString
public class ActionPush extends ByteCodeAction {
private List<ByteCodeItem> items;
@Override
public ActionField getType() {
return ActionField.Push;
}
}

@ -0,0 +1,15 @@
package com.alterdekim.flash.decompiler.action;
import com.alterdekim.flash.decompiler.util.ActionField;
import lombok.NoArgsConstructor;
import lombok.ToString;
@ToString
@NoArgsConstructor
public class ActionSetMember extends ByteCodeAction {
@Override
public ActionField getType() {
return ActionField.SetMember;
}
}

@ -0,0 +1,11 @@
package com.alterdekim.flash.decompiler.action;
import com.alterdekim.flash.decompiler.util.ActionField;
import lombok.NoArgsConstructor;
import lombok.ToString;
@ToString
@NoArgsConstructor
public abstract class ByteCodeAction {
public abstract ActionField getType();
}

@ -0,0 +1,27 @@
package com.alterdekim.flash.decompiler.item;
public enum ActionPushType {
STRING(0),
FLOAT(1),
NULL(2),
UNDEFINED(3),
REGISTER(4),
BOOLEAN(5),
DOUBLE(6),
INTEGER(7),
POOL_INDEX(8),
POOL_INDEX_BIG(9);
public final int type;
ActionPushType(int type) {
this.type = type;
}
public static ActionPushType fromType(int type) {
for( ActionPushType pushType : ActionPushType.values()) {
if( pushType.type == type ) return pushType;
}
return NULL;
}
}

@ -0,0 +1,11 @@
package com.alterdekim.flash.decompiler.item;
import lombok.NoArgsConstructor;
import lombok.ToString;
@NoArgsConstructor
@ToString
public abstract class ByteCodeItem {
public abstract ActionPushType getType();
public abstract Object getValue();
}

@ -0,0 +1,20 @@
package com.alterdekim.flash.decompiler.item;
import lombok.AllArgsConstructor;
import lombok.ToString;
@ToString
@AllArgsConstructor
public class IntegerItem extends ByteCodeItem {
private Integer val;
@Override
public ActionPushType getType() {
return ActionPushType.INTEGER;
}
@Override
public Object getValue() {
return val;
}
}

@ -0,0 +1,18 @@
package com.alterdekim.flash.decompiler.item;
import lombok.NoArgsConstructor;
import lombok.ToString;
@ToString
@NoArgsConstructor
public class NullItem extends ByteCodeItem {
@Override
public ActionPushType getType() {
return ActionPushType.NULL;
}
@Override
public Object getValue() {
return null;
}
}

@ -0,0 +1,21 @@
package com.alterdekim.flash.decompiler.item;
import lombok.AllArgsConstructor;
import lombok.ToString;
@ToString
@AllArgsConstructor
public class PoolIndexItem extends ByteCodeItem {
private Integer val;
@Override
public ActionPushType getType() {
return ActionPushType.POOL_INDEX;
}
@Override
public Object getValue() {
return val;
}
}

@ -0,0 +1,21 @@
package com.alterdekim.flash.decompiler.item;
import lombok.AllArgsConstructor;
import lombok.ToString;
@ToString
@AllArgsConstructor
public class StringItem extends ByteCodeItem {
private String val;
@Override
public ActionPushType getType() {
return ActionPushType.STRING;
}
@Override
public Object getValue() {
return val;
}
}

@ -0,0 +1,24 @@
package com.alterdekim.flash.decompiler.tag;
import com.alterdekim.flash.decompiler.action.ByteCodeAction;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@NoArgsConstructor
@ToString
@Getter
@Setter
public class DoAction extends ShockwaveTag {
private List<String> actionPool;
private List<ByteCodeAction> actions;
@Override
public TagType getType() {
return TagType.DoAction;
}
}

@ -0,0 +1,10 @@
package com.alterdekim.flash.decompiler.tag;
import lombok.NoArgsConstructor;
import lombok.ToString;
@ToString
@NoArgsConstructor
public abstract class ShockwaveTag {
public abstract TagType getType();
}

@ -0,0 +1,22 @@
package com.alterdekim.flash.decompiler.tag;
public enum TagType {
SetBackgroundColor(9),
DoAction(12),
ShowFrame(1),
End(0),
Unknown(-1);
private final int id;
TagType(int id) {
this.id = id;
}
public static TagType fromId(int id) {
for( TagType type : TagType.values()) {
if( type.id == id ) return type;
}
return Unknown;
}
}

@ -0,0 +1,104 @@
package com.alterdekim.flash.decompiler.translator;
import com.alterdekim.flash.decompiler.action.ActionPush;
import com.alterdekim.flash.decompiler.item.ActionPushType;
import com.alterdekim.flash.decompiler.action.ByteCodeAction;
import com.alterdekim.flash.decompiler.item.ByteCodeItem;
import com.alterdekim.flash.decompiler.item.PoolIndexItem;
import com.alterdekim.flash.decompiler.tag.DoAction;
import com.alterdekim.flash.decompiler.util.ActionField;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
@Slf4j
public class Flash2Java {
private List<String> pool;
public Map<String, FlashVar> convert(DoAction doAction) {
this.pool = doAction.getActionPool();
List<ByteCodeAction> actions = doAction.getActions();
List<ByteCodeAction> stack = new ArrayList<>();
Map<String, FlashVar> vars = new HashMap<>();
for (ByteCodeAction action : actions) {
try {
switch (action.getType()) {
case DefineLocal:
defineLocal(vars, stack);
stack.clear();
break;
case GetVariable:
getVariable(vars, stack);
stack.clear();
break;
case SetMember:
setMember(vars, stack);
stack.clear();
break;
default:
stack.add(action);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
return vars;
}
private void setMember(Map<String, FlashVar> vars, List<ByteCodeAction> stack) {
String varKey = vars.keySet().stream().filter(k -> vars.get(k).isActive()).findFirst().get();
FlashVar varVal = vars.get(varKey);
ActionPush push = (ActionPush) stack.get(0);
if( stack.size() == 1 ) {
Map<Integer, String> m = varVal.getVal() != null ? ((Map<Integer, String>) varVal.getVal()) : new HashMap<>();
m.put((Integer) push.getItems().get(0).getValue(), fromPool((Integer) push.getItems().get(1).getValue()));
varVal.setVal(m);
vars.put(varKey, varVal);
return;
}
Map<Integer, Map<String, Object>> m = varVal.getVal() != null ? ((Map<Integer, Map<String, Object>>) varVal.getVal()) : new HashMap<>();
Map<String, Object> v = new HashMap<>();
for( int i = 1; i < push.getItems().size()-1; i+=2 ) {
v.put(fromPool((Integer) push.getItems().get(i).getValue()), valConverter(push.getItems().get(i+1)));
}
m.put((Integer) push.getItems().get(0).getValue(), v);
varVal.setVal(m);
vars.put(varKey, varVal);
}
private Object valConverter(ByteCodeItem item) {
if ( item.getType() == ActionPushType.POOL_INDEX ) {
return fromPool( (Integer) item.getValue() );
}
return item.getValue();
}
private void getVariable(Map<String, FlashVar> vars, List<ByteCodeAction> stack) throws Exception {
if (stack.size() != 1 ) { throw new Exception("Bad getVariable format"); }
ActionPush push = (ActionPush) stack.get(0);
PoolIndexItem pi = (PoolIndexItem) push.getItems().get(0);
vars.keySet().forEach(k -> {
FlashVar fv = vars.get(k);
fv.setActive(false);
vars.put(k, fv);
});
String key = fromPool((int) pi.getValue());
FlashVar v = vars.get(key);
v.setActive(true);
vars.put(key, v);
}
private void defineLocal(Map<String, FlashVar> vars, List<ByteCodeAction> stack) throws Exception {
if (stack.size() != 2 || stack.get(1).getType() != ActionField.InitObject) { throw new Exception("Bad defineLocal format"); }
ActionPush push = (ActionPush) stack.get(0);
PoolIndexItem pi = (PoolIndexItem) push.getItems().get(0);
// todo: implement assignment ability
vars.put(fromPool((Integer) pi.getValue()), new FlashVar());
}
private String fromPool(int index) {
return pool.get(index);
}
}

@ -0,0 +1,15 @@
package com.alterdekim.flash.decompiler.translator;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class FlashVar {
private boolean isActive = false;
private Object val;
}

@ -0,0 +1,24 @@
package com.alterdekim.flash.decompiler.util;
public enum ActionField {
ConstantPool((byte) 0x88),
Push((byte) 0x96),
InitObject((byte) 0x43),
DefineLocal((byte) 0x3C),
SetMember((byte)0x4F),
GetVariable((byte) 0x1C),
Unknown((byte) 0x00);
public final byte id;
ActionField(byte id) {
this.id = id;
}
public static ActionField fromId(int id) {
for( ActionField type : ActionField.values()) {
if( type.id == id ) return type;
}
return Unknown;
}
}

@ -0,0 +1,29 @@
package com.alterdekim.flash.decompiler.util;
import lombok.extern.slf4j.Slf4j;
import java.util.BitSet;
@Slf4j
public class Bits {
public static int convert(BitSet bits, int len, boolean swapEndian) {
int n = 0;
for(int i = 0; i < len; i++ ) {
n += bits.get(i) ? 1 << (swapEndian ? i : (len-i-1)) : 0;
}
return n;
}
public static float convertP8(BitSet bits) {
float n = convert(bits.get(8, 16), 8, true);
for(int i = 0; i < 8; i++) {
n += bits.get(i) ? 1f / (float) (1 << i) : 0f;
}
return n;
}
public static int ceilToByte(int n) {
return (n % 8 == 0) ? n : n + (8 - (n % 8));
}
}

@ -0,0 +1,19 @@
package com.alterdekim.flash.decompiler.util;
public class ByteUtil {
public static int bytesToInt(byte[] bytes) {
int value = 0;
for (byte b : bytes) {
value = (value << 8) + (b & 0xFF);
}
return value;
}
public static int bytesToIntRev(byte[] bytes) {
int value = 0;
for(int i = bytes.length-1; i >= 0; i-- ) {
value = (value << 8) + (bytes[i] & 0xFF);
}
return value;
}
}

@ -0,0 +1,24 @@
package com.alterdekim.flash.decompiler.util;
import lzma.sdk.lzma.Decoder;
import lzma.streams.LzmaInputStream;
import org.apache.commons.io.IOUtils;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.zip.InflaterInputStream;
public class Compression {
public static byte[] decompressDEFLATE(byte[] data) throws IOException {
return IOUtils.toByteArray(new InflaterInputStream(new ByteArrayInputStream(data)));
}
public static byte[] decompressLZMA(byte[] data) throws IOException {
try (LzmaInputStream inputStream = new LzmaInputStream(
new BufferedInputStream(new ByteArrayInputStream(data)),
new Decoder())) {
return IOUtils.toByteArray(inputStream);
}
}
}

@ -0,0 +1,19 @@
package com.alterdekim.flash.decompiler.util;
public enum PayloadCompression {
NONE((byte) 0x46),
DEFLATE((byte) 0x43),
LZMA((byte) 0x5a);
public final byte tag;
PayloadCompression(byte tag) {
this.tag = tag;
}
public static PayloadCompression getByTag(byte tag) {
for( PayloadCompression compression : PayloadCompression.values() )
if( compression.tag == tag ) return compression;
return PayloadCompression.NONE;
}
}

@ -0,0 +1,15 @@
package com.alterdekim.flash.decompiler.util;
public class StringUtils {
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
}