AS2 detection of uninitialized class fields is parallel

This commit is contained in:
Jindra Petřík
2025-07-01 23:56:49 +02:00
parent a7e00e52bf
commit 752b242c58
4 changed files with 293 additions and 206 deletions

View File

@@ -16,6 +16,7 @@
*/
package com.jpexs.decompiler.flash.action.as2;
import com.jpexs.decompiler.flash.IdentifiersDeobfuscation;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.action.model.CallMethodActionItem;
import com.jpexs.decompiler.flash.action.model.DeleteActionItem;
@@ -33,7 +34,9 @@ import com.jpexs.decompiler.flash.helpers.collections.MyEntry;
import com.jpexs.decompiler.flash.tags.DoInitActionTag;
import com.jpexs.decompiler.flash.tags.base.ASMSource;
import com.jpexs.decompiler.graph.AbstractGraphTargetVisitor;
import com.jpexs.decompiler.graph.DottedChain;
import com.jpexs.decompiler.graph.GraphTargetItem;
import com.jpexs.helpers.Cache;
import com.jpexs.helpers.CancellableWorker;
import com.jpexs.helpers.ProgressListener;
import java.util.ArrayList;
@@ -59,9 +62,16 @@ public class UninitializedClassFieldsDetector {
progressListeners.remove(listener);
}
private void fireProgress(String status) {
private void fireProgress(ASMSource asm, String asmPath) {
if (asm instanceof DoInitActionTag) {
DoInitActionTag doi = (DoInitActionTag) asm;
String exportName = doi.getSwf().getExportName(doi.spriteId);
if (exportName != null) {
asmPath = DottedChain.parseNoSuffix(exportName).toPrintableString(false);
}
}
for (ProgressListener listener : progressListeners) {
listener.status(status);
listener.status(asmPath);
}
}
@@ -211,83 +221,158 @@ public class UninitializedClassFieldsDetector {
}
final Map<String, Map<String, Trait>> result = new LinkedHashMap<>();
Map<String, ASMSource> asms = swf.getASMs(false);
CancellableWorker worker = CancellableWorker.getCurrent();
/*Map<String, List<GraphTargetItem>> trees = new HashMap<>();
asms.entrySet().parallelStream()
.forEach(item -> {
CancellableWorker.assignThreadToWorker(Thread.currentThread(), worker);
if (worker.isCancelled()) {
return;
}
fireProgress(item.getValue(), item.getKey());
try {
trees.put(item.getKey(), item.getValue().getActionsToTree());
} catch (Throwable t) {
trees.put(item.getKey(), new ArrayList<>());
//ignore
}
}); */
List<ASMSource> classesAsms = new ArrayList<>();
final Map<String, Map<String, Trait>> classTraits = new LinkedHashMap<>(ActionScript2Classes.getClassToTraits());
final Map<String, List<String>> classInheritance = new HashMap<>(ActionScript2Classes.getClassInheritance());
//get all assigned traits and inheritance tree
for (String key : asms.keySet()) {
if (CancellableWorker.isInterrupted()) {
throw new InterruptedException();
}
ASMSource asm = asms.get(key);
if (asm instanceof DoInitActionTag) {
DoInitActionTag doi = (DoInitActionTag) asm;
String exportName = doi.getSwf().getCharacter(doi.getCharacterId()).getExportName();
if (exportName != null && exportName.startsWith("__Packages.")) {
fireProgress(key);
List<GraphTargetItem> tree = asm.getActionsToTree();
for (GraphTargetItem item : tree) {
if (item instanceof InterfaceActionItem) {
InterfaceActionItem iai = (InterfaceActionItem) item;
String className = String.join(".", getMembersPath(iai.name));
classInheritance.put(className, new ArrayList<>());
if (iai.superInterfaces != null) {
for (GraphTargetItem imp : iai.superInterfaces) {
String imtName = String.join(".", getMembersPath(imp));
classInheritance.get(className).add(imtName);
}
}
}
if (item instanceof ClassActionItem) {
ClassActionItem cai = (ClassActionItem) item;
final String className = String.join(".", getMembersPath(cai.className));
classInheritance.put(className, new ArrayList<>());
if (!classTraits.containsKey(className)) {
classTraits.put(className, new LinkedHashMap<>());
}
for (int i = 0; i < cai.traits.size(); i++) {
MyEntry<GraphTargetItem, GraphTargetItem> en = cai.traits.get(i);
if (!(en.getKey() instanceof DirectValueActionItem)) {
continue;
}
DirectValueActionItem dv = (DirectValueActionItem) en.getKey();
String name = dv.getAsString();
GraphTargetItem value = en.getValue();
boolean isStatic = cai.traitsStatic.get(i);
if (value instanceof FunctionActionItem) {
if (name.startsWith("__get__") || name.startsWith("__set__")) {
String vname = name.substring(7);
Variable v = new Variable(isStatic, vname, vname, className);
classTraits.get(className).put(vname, v);
Map<String, List<GraphTargetItem>> cachedTrees = new HashMap<>();
asms.entrySet().parallelStream()
.filter(item -> item.getValue() instanceof DoInitActionTag)
.forEach(entry -> {
CancellableWorker.assignThreadToWorker(Thread.currentThread(), worker);
if (worker.isCancelled()) {
return;
}
Method m = new Method(isStatic, name, "Unknown" /*FIXME?*/, className);
classTraits.get(className).put(name, m);
} else {
Variable v = new Variable(isStatic, name, name, className);
classTraits.get(className).put(name, v);
}
}
if (cai.extendsOp != null) {
String parentClassName = String.join(".", getMembersPath(cai.extendsOp));
classInheritance.get(className).add(parentClassName);
} else {
classInheritance.get(className).add("Object");
}
if (cai.implementsOp != null) {
for (GraphTargetItem imp : cai.implementsOp) {
String imtName = String.join(".", getMembersPath(imp));
classInheritance.get(className).add(imtName);
}
}
}
}
classesAsms.add(doi);
}
}
}
fireProgress(entry.getValue(), entry.getKey());
String key = entry.getKey();
ASMSource asm = asms.get(key);
if (asm instanceof DoInitActionTag) {
DoInitActionTag doi = (DoInitActionTag) asm;
String exportName = doi.getSwf().getCharacter(doi.getCharacterId()).getExportName();
if (exportName != null && exportName.startsWith("__Packages.")) {
//fireProgress(doi, key);
List<GraphTargetItem> tree = new ArrayList<>();
try {
tree = asm.getActionsToTree();
} catch (Throwable t) {
//ignore
}
cachedTrees.put(key, tree);
//get all assigned traits and inheritance tree
for (GraphTargetItem item : tree) {
if (item instanceof InterfaceActionItem) {
InterfaceActionItem iai = (InterfaceActionItem) item;
String className = String.join(".", getMembersPath(iai.name));
classInheritance.put(className, new ArrayList<>());
if (iai.superInterfaces != null) {
for (GraphTargetItem imp : iai.superInterfaces) {
String imtName = String.join(".", getMembersPath(imp));
classInheritance.get(className).add(imtName);
}
}
}
if (item instanceof ClassActionItem) {
ClassActionItem cai = (ClassActionItem) item;
final String className = String.join(".", getMembersPath(cai.className));
classInheritance.put(className, new ArrayList<>());
if (!classTraits.containsKey(className)) {
classTraits.put(className, new LinkedHashMap<>());
}
for (int i = 0; i < cai.traits.size(); i++) {
MyEntry<GraphTargetItem, GraphTargetItem> en = cai.traits.get(i);
if (!(en.getKey() instanceof DirectValueActionItem)) {
continue;
}
DirectValueActionItem dv = (DirectValueActionItem) en.getKey();
String name = dv.getAsString();
GraphTargetItem value = en.getValue();
boolean isStatic = cai.traitsStatic.get(i);
if (value instanceof FunctionActionItem) {
if (name.startsWith("__get__") || name.startsWith("__set__")) {
String vname = name.substring(7);
Variable v = new Variable(isStatic, vname, vname, className);
classTraits.get(className).put(vname, v);
}
Method m = new Method(isStatic, name, "Unknown" /*FIXME?*/, className);
classTraits.get(className).put(name, m);
} else {
Variable v = new Variable(isStatic, name, name, className);
classTraits.get(className).put(name, v);
}
}
if (cai.extendsOp != null) {
String parentClassName = String.join(".", getMembersPath(cai.extendsOp));
classInheritance.get(className).add(parentClassName);
} else {
classInheritance.get(className).add("Object");
}
if (cai.implementsOp != null) {
for (GraphTargetItem imp : cai.implementsOp) {
String imtName = String.join(".", getMembersPath(imp));
classInheritance.get(className).add(imtName);
}
}
}
}
//Detect this.x assigns
for (GraphTargetItem item : tree) {
if (item instanceof ClassActionItem) {
ClassActionItem cai = (ClassActionItem) item;
final String className = String.join(".", getMembersPath(cai.className));
for (int i = 0; i < cai.traits.size(); i++) {
MyEntry<GraphTargetItem, GraphTargetItem> en = cai.traits.get(i);
if (!(en.getKey() instanceof DirectValueActionItem)) {
continue;
}
GraphTargetItem value = en.getValue();
if (value instanceof GraphTargetItem) {
AbstractGraphTargetVisitor visitor = new AbstractGraphTargetVisitor() {
@Override
public boolean visit(GraphTargetItem item) {
List<String> path = getFullPath(item);
if (path != null) {
List<String> parent = new ArrayList<>(path);
parent.remove(parent.size() - 1);
if (parent.size() == 1) {
if (parent.get(0).equals("this")) {
String name = path.get(path.size() - 1);
if (!containsTrait(classTraits, classInheritance, className, name) && (!result.containsKey(className) || !result.get(className).containsKey(name))) {
Variable v = new Variable(false, name, null, className);
if (!result.containsKey(className)) {
result.put(className, new LinkedHashMap<>());
}
result.get(className).put(name, v);
}
}
}
}
return true;
}
};
visitor.visit(value);
value.visitRecursively(visitor);
}
}
}
}
}
}
});
//Complete inheritance tree
for (String className : classInheritance.keySet()) {
@@ -301,73 +386,28 @@ public class UninitializedClassFieldsDetector {
}
}
}
}
//Detect this.x assigns
for (String key : asms.keySet()) {
if (CancellableWorker.isInterrupted()) {
throw new InterruptedException();
}
ASMSource asm = asms.get(key);
if (asm instanceof DoInitActionTag) {
DoInitActionTag doi = (DoInitActionTag) asm;
String exportName = doi.getSwf().getCharacter(doi.getCharacterId()).getExportName();
if (exportName != null && exportName.startsWith("__Packages.")) {
fireProgress(key);
List<GraphTargetItem> tree = asm.getActionsToTree();
for (GraphTargetItem item : tree) {
if (item instanceof ClassActionItem) {
ClassActionItem cai = (ClassActionItem) item;
final String className = String.join(".", getMembersPath(cai.className));
for (int i = 0; i < cai.traits.size(); i++) {
MyEntry<GraphTargetItem, GraphTargetItem> en = cai.traits.get(i);
if (!(en.getKey() instanceof DirectValueActionItem)) {
continue;
}
GraphTargetItem value = en.getValue();
if (value instanceof GraphTargetItem) {
AbstractGraphTargetVisitor visitor = new AbstractGraphTargetVisitor() {
@Override
public boolean visit(GraphTargetItem item) {
List<String> path = getFullPath(item);
if (path != null) {
List<String> parent = new ArrayList<>(path);
parent.remove(parent.size() - 1);
if (parent.size() == 1) {
if (parent.get(0).equals("this")) {
String name = path.get(path.size() - 1);
if (!containsTrait(classTraits, classInheritance, className, name) && (!result.containsKey(className) || !result.get(className).containsKey(name))) {
Variable v = new Variable(false, name, null, className);
if (!result.containsKey(className)) {
result.put(className, new LinkedHashMap<>());
}
result.get(className).put(name, v);
}
}
}
}
return true;
}
};
visitor.visit(value);
value.visitRecursively(visitor);
}
}
}
}
classesAsms.add(doi);
}
}
}
}
//getting static classname.x assigns
for (String key : asms.keySet()) {
if (CancellableWorker.isInterrupted()) {
throw new InterruptedException();
asms.entrySet().parallelStream().forEach(entry -> {
CancellableWorker.assignThreadToWorker(Thread.currentThread(), worker);
if (worker.isCancelled()) {
return;
}
fireProgress(entry.getValue(), entry.getKey());
String key = entry.getKey();
ASMSource asm = asms.get(key);
fireProgress(key);
List<GraphTargetItem> tree = asm.getActionsToTree();
//fireProgress(asm, key);
List<GraphTargetItem> tree = new ArrayList<>();
if (cachedTrees.containsKey(key)) {
tree = cachedTrees.get(key);
} else {
try {
tree = asm.getActionsToTree();
} catch(Throwable t) {
//ignore
}
}
for (GraphTargetItem item : tree) {
AbstractGraphTargetVisitor visitor = new AbstractGraphTargetVisitor() {
@Override
@@ -402,7 +442,7 @@ public class UninitializedClassFieldsDetector {
visitor.visit(item);
item.visitRecursively(visitor);
}
}
});
//Removed cached version of classes - allow reparsing using detected uninitialized fields

View File

@@ -24,12 +24,15 @@ import com.jpexs.decompiler.flash.exporters.settings.ScriptExportSettings;
import com.jpexs.decompiler.flash.tags.base.ASMSource;
import com.jpexs.helpers.CancellableWorker;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.ProgressListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -93,87 +96,127 @@ public class AS2ScriptExporter {
List<ExportScriptTask> tasks = new ArrayList<>();
String[] keys = asms.keySet().toArray(new String[asms.size()]);
Set<SWF> swfsThatNeedUninitializedClassTraitsDetection = new HashSet<>();
for (String key : keys) {
ASMSource asm = asms.get(key);
String currentOutDir = outdir + key + File.separator;
currentOutDir = new File(currentOutDir).getParentFile().toString() + File.separator;
List<String> existingNames = existingNamesMap.get(currentOutDir);
if (existingNames == null) {
existingNames = new ArrayList<>();
existingNamesMap.put(currentOutDir, existingNames);
if (asm.getSwf().needsCalculatingAS2UninitializeClassTraits(asm)) {
swfsThatNeedUninitializedClassTraitsDetection.add(asm.getSwf());
}
String name = Helper.makeFileName(asm.getExportFileName());
int i = 1;
String baseName = name;
while (existingNames.contains(name)) {
i++;
name = baseName + "_" + i;
}
existingNames.add(name);
tasks.add(new ExportScriptTask(handler, cnt++, asms.size(), name, asm, currentOutDir, exportSettings, evl));
}
if (!parallel || tasks.size() < 2) {
try {
CancellableWorker.call("as2scriptexport", new Callable<Void>() {
@Override
public Void call() throws Exception {
for (ExportScriptTask task : tasks) {
if (CancellableWorker.isInterrupted()) {
throw new InterruptedException();
}
ret.add(task.call());
}
return null;
}
}, Configuration.exportTimeout.get(), TimeUnit.SECONDS);
} catch (TimeoutException ex) {
logger.log(Level.SEVERE, Helper.formatTimeToText(Configuration.exportTimeout.get()) + " ActionScript export limit reached", ex);
} catch (ExecutionException | InterruptedException ex) {
logger.log(Level.SEVERE, "Error during AS2 export", ex);
ProgressListener progressListener = new ProgressListener() {
@Override
public void progress(int p) {
}
} else {
ExecutorService executor = Executors.newFixedThreadPool(Configuration.getParallelThreadCount());
List<Future<File>> futureResults = new ArrayList<>();
for (ExportScriptTask task : tasks) {
Future<File> future = executor.submit(task);
futureResults.add(future);
}
try {
executor.shutdown();
if (!executor.awaitTermination(Configuration.exportTimeout.get(), TimeUnit.SECONDS)) {
logger.log(Level.SEVERE, "{0} ActionScript export limit reached", Helper.formatTimeToText(Configuration.exportTimeout.get()));
for (ExportScriptTask task : tasks) {
CancellableWorker.cancelThread(task.thread);
}
@Override
public void status(String status) {
if (evl != null) {
evl.handleEvent("unitializedClassFields", status);
}
} catch (InterruptedException ex) {
//ignored
} finally {
executor.shutdownNow();
}
};
for (SWF swf : swfsThatNeedUninitializedClassTraitsDetection) {
swf.getUninitializedClassFieldsDetector().addProgressListener(progressListener);
}
try {
for (SWF swf : swfsThatNeedUninitializedClassTraitsDetection) {
swf.calculateAs2UninitializedClassTraits();
}
} catch (InterruptedException ie) {
return ret;
}
try {
for (String key : keys) {
ASMSource asm = asms.get(key);
String currentOutDir = outdir + key + File.separator;
currentOutDir = new File(currentOutDir).getParentFile().toString() + File.separator;
List<String> existingNames = existingNamesMap.get(currentOutDir);
if (existingNames == null) {
existingNames = new ArrayList<>();
existingNamesMap.put(currentOutDir, existingNames);
}
String name = Helper.makeFileName(asm.getExportFileName());
int i = 1;
String baseName = name;
while (existingNames.contains(name)) {
i++;
name = baseName + "_" + i;
}
existingNames.add(name);
tasks.add(new ExportScriptTask(handler, cnt++, asms.size(), name, asm, currentOutDir, exportSettings, evl));
}
for (int f = 0; f < futureResults.size(); f++) {
if (!parallel || tasks.size() < 2) {
try {
if (futureResults.get(f).isDone()) {
ret.add(futureResults.get(f).get());
CancellableWorker.call("as2scriptexport", new Callable<Void>() {
@Override
public Void call() throws Exception {
for (ExportScriptTask task : tasks) {
if (CancellableWorker.isInterrupted()) {
throw new InterruptedException();
}
ret.add(task.call());
}
return null;
}
}, Configuration.exportTimeout.get(), TimeUnit.SECONDS);
} catch (TimeoutException ex) {
logger.log(Level.SEVERE, Helper.formatTimeToText(Configuration.exportTimeout.get()) + " ActionScript export limit reached", ex);
} catch (ExecutionException | InterruptedException ex) {
logger.log(Level.SEVERE, "Error during AS2 export", ex);
}
} else {
ExecutorService executor = Executors.newFixedThreadPool(Configuration.getParallelThreadCount());
List<Future<File>> futureResults = new ArrayList<>();
for (ExportScriptTask task : tasks) {
Future<File> future = executor.submit(task);
futureResults.add(future);
}
try {
executor.shutdown();
if (!executor.awaitTermination(Configuration.exportTimeout.get(), TimeUnit.SECONDS)) {
logger.log(Level.SEVERE, "{0} ActionScript export limit reached", Helper.formatTimeToText(Configuration.exportTimeout.get()));
for (ExportScriptTask task : tasks) {
CancellableWorker.cancelThread(task.thread);
}
}
} catch (InterruptedException ex) {
//ignored
} catch (ExecutionException ex) {
if (!(ex.getCause() instanceof InterruptedException)) {
logger.log(Level.SEVERE, "Error during ActionScript export", ex);
}
} finally {
executor.shutdownNow();
}
}
}
return ret;
for (int f = 0; f < futureResults.size(); f++) {
try {
if (futureResults.get(f).isDone()) {
ret.add(futureResults.get(f).get());
}
} catch (InterruptedException ex) {
//ignored
} catch (ExecutionException ex) {
if (!(ex.getCause() instanceof InterruptedException)) {
logger.log(Level.SEVERE, "Error during ActionScript export", ex);
}
}
}
}
return ret;
} finally {
for (SWF swf : swfsThatNeedUninitializedClassTraitsDetection) {
swf.getUninitializedClassFieldsDetector().removeProgressListener(progressListener);
}
}
}
}