From 752b242c58a9341224018903a3f1d5bc135dd044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Tue, 1 Jul 2025 23:56:49 +0200 Subject: [PATCH] AS2 detection of uninitialized class fields is parallel --- CHANGELOG.md | 3 +- .../as2/UninitializedClassFieldsDetector.java | 316 ++++++++++-------- .../exporters/script/AS2ScriptExporter.java | 177 ++++++---- src/com/jpexs/decompiler/flash/gui/Main.java | 3 + 4 files changed, 293 insertions(+), 206 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 093baef9e..bcdda5fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added - [#2477] Option to disable AS2 detection of uninitialized class fields -- AS2 detection of uninitialized class fields is cancellable and shows progress +- AS2 detection of uninitialized class fields is cancellable, faster (parallel) + and shows its progress - AS2 - show deobfuscated class/package names in the class tree ### Fixed diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/as2/UninitializedClassFieldsDetector.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/as2/UninitializedClassFieldsDetector.java index a07e29da3..c7ff75791 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/as2/UninitializedClassFieldsDetector.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/as2/UninitializedClassFieldsDetector.java @@ -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> result = new LinkedHashMap<>(); Map asms = swf.getASMs(false); + + CancellableWorker worker = CancellableWorker.getCurrent(); + + /*Map> 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 classesAsms = new ArrayList<>(); - + final Map> classTraits = new LinkedHashMap<>(ActionScript2Classes.getClassToTraits()); final Map> 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 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 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> 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 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 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 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 path = getFullPath(item); + if (path != null) { + List 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 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 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 path = getFullPath(item); - if (path != null) { - List 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 tree = asm.getActionsToTree(); + //fireProgress(asm, key); + List 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 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/script/AS2ScriptExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/script/AS2ScriptExporter.java index 2025218ac..dc1b48181 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/script/AS2ScriptExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/script/AS2ScriptExporter.java @@ -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 tasks = new ArrayList<>(); String[] keys = asms.keySet().toArray(new String[asms.size()]); + Set 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 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() { - @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> futureResults = new ArrayList<>(); - for (ExportScriptTask task : tasks) { - Future 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 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() { + @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> futureResults = new ArrayList<>(); + for (ExportScriptTask task : tasks) { + Future 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); + } + } } } diff --git a/src/com/jpexs/decompiler/flash/gui/Main.java b/src/com/jpexs/decompiler/flash/gui/Main.java index ae93512b6..ddb7cd275 100644 --- a/src/com/jpexs/decompiler/flash/gui/Main.java +++ b/src/com/jpexs/decompiler/flash/gui/Main.java @@ -1528,6 +1528,9 @@ public class Main { if (event.equals("importing_as")) { startWork(AppStrings.translate("work.importing_as") + "..." + (String) data, importWorker); } + if (event.equals("unitializedClassFields")) { + continueWork(AppStrings.translate("work.decompiling") + "..." + (String) data); + } } }); }