package com.jpexs.decompiler.flash.flexsdk; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.abc.ABC; import com.jpexs.decompiler.flash.abc.ScriptPack; import com.jpexs.decompiler.flash.abc.types.InstanceInfo; import com.jpexs.decompiler.flash.abc.types.ScriptInfo; import com.jpexs.decompiler.flash.abc.types.traits.Trait; import com.jpexs.decompiler.flash.abc.types.traits.TraitClass; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; import com.jpexs.decompiler.flash.exporters.script.AS3ScriptExporter; import com.jpexs.decompiler.flash.exporters.settings.ScriptExportSettings; import com.jpexs.decompiler.flash.exporters.swf.SwfToSwcExporter; import com.jpexs.decompiler.flash.importers.As3ScriptReplaceException; import com.jpexs.decompiler.flash.importers.As3ScriptReplaceExceptionItem; import com.jpexs.decompiler.flash.importers.As3ScriptReplacerInterface; import com.jpexs.decompiler.flash.tags.ABCContainerTag; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.graph.DottedChain; import com.jpexs.helpers.Helper; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class MxmlcAs3ScriptReplacer extends MxmlcRunner implements As3ScriptReplacerInterface { private ScriptPack initedPack; private File tempDir; private File pkgDir; private File swcFile; public MxmlcAs3ScriptReplacer(String flexSdkPath) { super(flexSdkPath); } private synchronized boolean isInited(ScriptPack pack) { return tempDir != null && initedPack == pack; } private static void deleteFolder(File folder) { File[] files = folder.listFiles(); if (files != null) { //some JVMs return null for empty dirs for (File f : files) { if (f.isDirectory()) { deleteFolder(f); } else { f.delete(); } } } folder.delete(); } private SWF recompileSWF(SWF swf) throws IOException, InterruptedException { ByteArrayOutputStream swfOrigBaos = new ByteArrayOutputStream(); swf.saveTo(swfOrigBaos); return new SWF(new ByteArrayInputStream(swfOrigBaos.toByteArray()), Configuration.parallelSpeedUp.get(), false); } private boolean isParentDeleted(ABC abc, List<ABC> allAbcs, DottedChain className) { int class_index = abc.findClassByName(className); if (class_index == -1) { for (ABC a : allAbcs) { class_index = abc.findClassByName(className); if (class_index != -1) { abc = a; break; } } if (class_index == -1) { return false; } } InstanceInfo ii = abc.instance_info.get(class_index); if (ii.deleted) { return true; } if (ii.super_index != 0 && isParentDeleted(abc, allAbcs, abc.constants.getMultiname(ii.super_index).getNameWithNamespace(abc.constants, false))) { return true; } for (int iface : ii.interfaces) { if (isParentDeleted(abc, allAbcs, abc.constants.getMultiname(iface).getNameWithNamespace(abc.constants, false))) { return true; } } return false; } @Override public synchronized void replaceScript(ScriptPack pack, String text) throws As3ScriptReplaceException, IOException, InterruptedException { if (!pack.isSimple) { throw new IOException("Cannot compile such file"); //Alchemy, etc. TODO: handle better } if (!isInited(pack)) { initReplacement(pack); } File scriptFileToCompile = new File(pkgDir, pack.getClassPath().className + ".as"); //Write new script Helper.writeFile(scriptFileToCompile.getAbsolutePath(), text.getBytes("UTF-8")); File compiledSwfFile = new File(pkgDir, "out.swf"); try { //Compile it (and subclasses stubs) mxmlc("-strict=false", "-include-inheritance-dependencies-only", "-warnings=false", "-library-path", swcFile.getAbsolutePath(), "-source-path", tempDir.getAbsolutePath(), "-output", compiledSwfFile.getAbsolutePath(), "-debug=true", scriptFileToCompile.getAbsolutePath()); } catch (MxmlcException ex1) { //String compiledFilePath = scriptFileToCompile.getAbsolutePath(); Pattern errPattern = Pattern.compile("^" + Pattern.quote(tempDir.getAbsolutePath()) + "(?<file>.*)\\((?<line>[0-9]+)\\): col: (?<col>[0-9]+) (?<message>.*)$"); String err = ex1.getMxmlcErrorOutput(); String errLines[] = err.split("\r?\n"); List<As3ScriptReplaceExceptionItem> errorItems = new ArrayList<>(); for (int i = 0; i < errLines.length; i++) { String line = errLines[i].trim(); Matcher m = errPattern.matcher(line); if (m.matches()) { String errFile = m.group("file"); int errLine = Integer.parseInt(m.group("line")); int errCol = Integer.parseInt(m.group("col")); String errMsg = m.group("message"); errorItems.add(new As3ScriptReplaceExceptionItem(errFile, errMsg, errLine, errCol)); } } throw new As3ScriptReplaceException(errorItems); } try (FileInputStream fis = new FileInputStream(compiledSwfFile)) { SWF newSWF = new SWF(fis, false, false); List<ABCContainerTag> newTags = newSWF.getAbcList(); int oldScriptIndex = pack.scriptIndex; int oldClassIndex = -1; ScriptInfo oldScriptInfo = pack.abc.script_info.get(pack.scriptIndex); for (Trait t : oldScriptInfo.traits.traits) { if (t instanceof TraitClass) { int traitClassIndex = ((TraitClass) t).class_info; if (oldClassIndex == -1 || traitClassIndex < oldClassIndex) { oldClassIndex = traitClassIndex; } } } if (pack.isSimple) { oldScriptInfo.delete(pack.abc, true); } else { //NOO } pack.abc.pack(); // removes old classes/methods/scripts ABCContainerTag newTagsLast = newTags.get(newTags.size() - 1); ABC newLastAbc = newTagsLast.getABC(); Map<Integer, Integer> classesMap = new HashMap<>(); Map<Integer, Integer> scriptsMap = new HashMap<>(); pack.abc.mergeABC(newLastAbc, new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), classesMap, new HashMap<>(), scriptsMap ); //Reorder newly created scripts to be in place //where old script was List<Integer> addedScriptIndices = new ArrayList<>(scriptsMap.values()); Collections.sort(addedScriptIndices); List<ScriptInfo> addedScripts = new ArrayList<>(); for (int i = addedScriptIndices.size() - 1; i >= 0; i--) { int newScriptIndex = addedScriptIndices.get(i); addedScripts.add(0, pack.abc.script_info.remove(newScriptIndex)); } for (int i = 0; i < addedScripts.size(); i++) { pack.abc.script_info.add(oldScriptIndex + i, addedScripts.get(i)); } //IMPORTANT: Map newly created classes to their position as they //were in original script because FlashPlayer needs //parent class to be defined earlier if (oldClassIndex > -1) { List<Integer> addedClassIndices = new ArrayList<>(classesMap.values()); Collections.sort(addedClassIndices); int totalClassCount = pack.abc.class_info.size(); Map<Integer, Integer> classesRemap = new HashMap<>(); for (int i = 0; i < addedClassIndices.size(); i++) { classesRemap.put(addedClassIndices.get(i), oldClassIndex + i); } int mappingStart = oldClassIndex; for (int i = oldClassIndex; i < totalClassCount; i++) { if (!classesRemap.containsKey(i)) { for (int j = mappingStart; j < totalClassCount; j++) { if (!classesRemap.containsValue(j)) { classesRemap.put(i, j); mappingStart = j + 1; break; } } } } pack.abc.reorganizeClasses(classesRemap); } ((Tag) pack.abc.parentTag).setModified(true); deinitReplacement(pack);//successfull finish } } @Override public boolean isAvailable() { String flexLocation = Configuration.flexSdkLocation.get(); return !(flexLocation.isEmpty() || (!new File(MxmlcRunner.getMxmlcPath(flexLocation)).exists())); } @Override public synchronized void initReplacement(ScriptPack pack) { if (tempDir != null) { deinitReplacement(pack); } try { tempDir = Files.createTempDirectory("ffdec-mxmlc-replace").toFile(); pkgDir = tempDir; for (String pkgPart : pack.getClassPath().packageStr.toList()) { if (!pkgPart.isEmpty()) { pkgDir = new File(pkgDir, pkgPart); } } pkgDir.mkdirs(); swcFile = new File(pkgDir, "out.swc"); //Make copy without the old script SWF swfCopy = recompileSWF(pack.getSwf()); List<ABC> modAbcs = new ArrayList<>(); List<ScriptPack> copyPacks = swfCopy.getAS3Packs(); for (ScriptPack sp : copyPacks) { if (sp.getClassPath().equals(pack.getClassPath())) { sp.abc.script_info.get(sp.scriptIndex).delete(sp.abc, true); ((Tag) sp.abc.parentTag).setModified(true); modAbcs.add(sp.abc); break; } } List<ScriptPack> removedPacks = new ArrayList<>(); //remove all subclasses from the SWC for (ScriptPack sp : copyPacks) { DottedChain dc = sp.getPathPackage().add(sp.getPathScriptName(), ""); if (isParentDeleted(sp.abc, sp.allABCs, dc)) { sp.abc.script_info.get(sp.scriptIndex).delete(sp.abc, true); modAbcs.add(sp.abc); removedPacks.add(sp); } } //Export subclasses so they can be compiled by Flex, but ONLY STUBS. //No method code to avoid code compilation problems. //This compiled code won't be used at all in original SWF, //it is used only by Flex to properly compile current script AS3ScriptExporter ex = new AS3ScriptExporter(); ex.exportActionScript3(swfCopy, null, tempDir.getAbsolutePath(), removedPacks, new ScriptExportSettings(ScriptExportMode.AS_METHOD_STUBS, false), false, null); //now really remove the classes from SWF copy for (ABC a : modAbcs) { a.pack(); } //Generate SWC file from the modified SWF file. //Flex then uses the code already present in the SWC, no need to decompile it (hurray!) SwfToSwcExporter swcExport = new SwfToSwcExporter(); swcExport.exportSwf(swfCopy, swcFile, true); } catch (IOException iex) { //ignore } catch (InterruptedException ex) { //ignore } this.initedPack = pack; } @Override public synchronized void deinitReplacement(ScriptPack pack) { if (tempDir != null && tempDir.exists()) { deleteFolder(tempDir); } tempDir = null; } }