diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java index 7ade5e28f..f17f66ae3 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java @@ -2507,8 +2507,7 @@ public final class SWF implements SWFContainerItem, Timelined { for (Tag tag : getTags()) { if (tag instanceof ImageTag) { ((ImageTag) tag).clearCache(); - } - else if (tag instanceof DefineCompactedFont) { + } else if (tag instanceof DefineCompactedFont) { ((DefineCompactedFont) tag).rebuildShapeCache(); } } @@ -2891,15 +2890,15 @@ public final class SWF implements SWFContainerItem, Timelined { timelined.setModified(true); timelined.resetTimeline(); } else // timeline should be always the swf here - if (removeDependencies) { - removeTagWithDependenciesFromTimeline(tag, timelined.getTimeline()); - timelined.setModified(true); - } else { - boolean modified = removeTagFromTimeline(tag, timelined.getTimeline()); - if (modified) { + if (removeDependencies) { + removeTagWithDependenciesFromTimeline(tag, timelined.getTimeline()); timelined.setModified(true); + } else { + boolean modified = removeTagFromTimeline(tag, timelined.getTimeline()); + if (modified) { + timelined.setModified(true); + } } - } } @Override @@ -3648,4 +3647,10 @@ public final class SWF implements SWFContainerItem, Timelined { } return null; } + + @Override + public void replaceTag(int index, Tag newTag) { + removeTag(index); + addTag(index, newTag); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSpriteTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSpriteTag.java index 733d1950c..01aaa1815 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSpriteTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSpriteTag.java @@ -424,4 +424,10 @@ public class DefineSpriteTag extends DrawableTag implements Timelined { public void clearReadOnlyListCache() { readOnlyTags = null; } + + @Override + public void replaceTag(int index, Tag newTag) { + removeTag(index); + addTag(index, newTag); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject2Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject2Tag.java index a4604ced5..7b3b7a748 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject2Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject2Tag.java @@ -442,4 +442,14 @@ public class PlaceObject2Tag extends PlaceObjectTypeTag implements ASMSourceCont public Amf3Value getAmfData() { return null; } + + @Override + public Integer getBitmapCache() { + return null; + } + + @Override + public Integer getVisible() { + return null; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject3Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject3Tag.java index dddf83f8b..45583dd94 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject3Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject3Tag.java @@ -240,12 +240,12 @@ public class PlaceObject3Tag extends PlaceObjectTypeTag implements ASMSourceCont super(swf, ID, NAME, null); } - public PlaceObject3Tag(SWF swf, boolean placeFlagMove, int depth, String className, int characterId, MATRIX matrix, CXFORMWITHALPHA colorTransform, int ratio, String name, int clipDepth, List surfaceFilterList, int blendMode, int bitmapCache, int visible, RGBA backgroundColor, CLIPACTIONS clipActions) { + public PlaceObject3Tag(SWF swf, boolean placeFlagMove, int depth, String className, int characterId, MATRIX matrix, CXFORMWITHALPHA colorTransform, int ratio, String name, int clipDepth, List surfaceFilterList, int blendMode, Integer bitmapCache, int visible, RGBA backgroundColor, CLIPACTIONS clipActions) { super(swf, ID, NAME, null); this.placeFlagHasClassName = className != null; this.placeFlagHasFilterList = surfaceFilterList != null; this.placeFlagHasBlendMode = blendMode >= 0; - this.placeFlagHasCacheAsBitmap = bitmapCache >= 0; + this.placeFlagHasCacheAsBitmap = bitmapCache != null; this.placeFlagHasVisible = visible >= 0; this.placeFlagOpaqueBackground = backgroundColor != null; this.placeFlagHasClipActions = clipActions != null; @@ -266,7 +266,7 @@ public class PlaceObject3Tag extends PlaceObjectTypeTag implements ASMSourceCont this.clipDepth = clipDepth; this.surfaceFilterList = surfaceFilterList; this.blendMode = blendMode; - this.bitmapCache = bitmapCache; + this.bitmapCache = bitmapCache == null ? 0 : bitmapCache; this.visible = visible; this.backgroundColor = backgroundColor; this.clipActions = clipActions; @@ -552,6 +552,14 @@ public class PlaceObject3Tag extends PlaceObjectTypeTag implements ASMSourceCont return true; } + @Override + public Integer getVisible() { + if (placeFlagHasVisible) { + return visible; + } + return null; + } + @Override public RGBA getBackgroundColor() { if (placeFlagOpaqueBackground) { @@ -622,4 +630,11 @@ public class PlaceObject3Tag extends PlaceObjectTypeTag implements ASMSourceCont return null; } + @Override + public Integer getBitmapCache() { + if (placeFlagHasCacheAsBitmap) { + return bitmapCache; + } + return null; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject4Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject4Tag.java index 22c51c9c3..87d8d1758 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject4Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObject4Tag.java @@ -246,13 +246,13 @@ public class PlaceObject4Tag extends PlaceObjectTypeTag implements ASMSourceCont super(swf, ID, NAME, null); } - public PlaceObject4Tag(SWF swf, boolean placeFlagMove, int depth, String className, int characterId, MATRIX matrix, CXFORMWITHALPHA colorTransform, int ratio, String name, int clipDepth, List surfaceFilterList, int blendMode, int bitmapCache, int visible, RGBA backgroundColor, CLIPACTIONS clipActions, Amf3Value amfData) { + public PlaceObject4Tag(SWF swf, boolean placeFlagMove, int depth, String className, int characterId, MATRIX matrix, CXFORMWITHALPHA colorTransform, int ratio, String name, int clipDepth, List surfaceFilterList, int blendMode, Integer bitmapCache, Integer visible, RGBA backgroundColor, CLIPACTIONS clipActions, Amf3Value amfData) { super(swf, ID, NAME, null); this.placeFlagHasClassName = className != null; this.placeFlagHasFilterList = surfaceFilterList != null; this.placeFlagHasBlendMode = blendMode >= 0; - this.placeFlagHasCacheAsBitmap = bitmapCache >= 0; - this.placeFlagHasVisible = visible >= 0; + this.placeFlagHasCacheAsBitmap = bitmapCache != null; + this.placeFlagHasVisible = visible != null; this.placeFlagOpaqueBackground = backgroundColor != null; this.placeFlagHasClipActions = clipActions != null; this.placeFlagHasClipDepth = clipDepth >= 0; @@ -272,8 +272,8 @@ public class PlaceObject4Tag extends PlaceObjectTypeTag implements ASMSourceCont this.clipDepth = clipDepth; this.surfaceFilterList = surfaceFilterList; this.blendMode = blendMode; - this.bitmapCache = bitmapCache; - this.visible = visible; + this.bitmapCache = bitmapCache == null ? 0 : bitmapCache; + this.visible = visible == null ? 0 : visible; this.backgroundColor = backgroundColor; this.clipActions = clipActions; this.amfData = amfData; @@ -644,4 +644,19 @@ public class PlaceObject4Tag extends PlaceObjectTypeTag implements ASMSourceCont return amfData; } + @Override + public Integer getBitmapCache() { + if (placeFlagHasCacheAsBitmap) { + return bitmapCache; + } + return null; + } + + @Override + public Integer getVisible() { + if (placeFlagHasVisible) { + return visible; + } + return null; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObjectTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObjectTag.java index c3a4c3d91..2d415e552 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObjectTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/PlaceObjectTag.java @@ -262,4 +262,13 @@ public class PlaceObjectTag extends PlaceObjectTypeTag { return null; } + @Override + public Integer getBitmapCache() { + return null; + } + + @Override + public Integer getVisible() { + return null; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ButtonTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ButtonTag.java index 0eaf822cd..108fc07dd 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ButtonTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ButtonTag.java @@ -166,4 +166,10 @@ public abstract class ButtonTag extends DrawableTag implements Timelined { @Override public void addTag(int index, Tag tag) { } + + @Override + public void replaceTag(int index, Tag newTag) { + removeTag(index); + addTag(index, newTag); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java index 9aa8603f2..e20fa8258 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java @@ -65,8 +65,12 @@ public abstract class PlaceObjectTypeTag extends Tag implements CharacterIdTag { public abstract boolean cacheAsBitmap(); + public abstract Integer getBitmapCache(); + public abstract boolean isVisible(); + public abstract Integer getVisible(); + public abstract RGBA getBackgroundColor(); public abstract boolean flagMove(); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java index 03aa57e68..6df236039 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java @@ -41,4 +41,6 @@ public interface Timelined extends BoundedTag { public void addTag(Tag tag); public void addTag(int index, Tag tag); + + public void replaceTag(int index, Tag newTag); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java index f3302096b..86731957e 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/Helper.java @@ -148,8 +148,8 @@ public class Helper { } /** - * Formats specified address to four numbers xxxx - * (or five numbers when showing decimal addresses) + * Formats specified address to four numbers xxxx (or five numbers when + * showing decimal addresses) * * @param number Address to format * @return String representation of the address @@ -159,8 +159,8 @@ public class Helper { } /** - * Formats specified address to four numbers xxxx - * (or five numbers when showing decimal addresses) + * Formats specified address to four numbers xxxx (or five numbers when + * showing decimal addresses) * * @param number Address to format * @param decimal Use decimal format @@ -648,7 +648,7 @@ public class Helper { for (String f : file) { try (FileInputStream fis = new FileInputStream(f)) { byte[] buf = new byte[4096]; - int cnt = 0; + int cnt; while ((cnt = fis.read(buf)) > 0) { baos.write(buf, 0, cnt); } @@ -659,6 +659,41 @@ public class Helper { return baos.toByteArray(); } + public static byte[] readFileEx(String... file) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (String f : file) { + FileInputStream fis = null; + try { + fis = new FileInputStream(f); + + byte[] buf = new byte[4096]; + int cnt; + while ((cnt = fis.read(buf)) > 0) { + baos.write(buf, 0, cnt); + } + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ex) { + //ignore + } + } + } + } + return baos.toByteArray(); + } + + public static String readTextFileEx(String... file) throws IOException { + byte[] data = readFileEx(file); + if (data.length > 1 && data[0] == (byte) 0xef && data[1] == (byte) 0xbb && data[2] == (byte) 0xbf) { + // remove UTF-8 BOM + return new String(data, 3, data.length - 3, Utf8Helper.charset); + } + + return new String(data, Utf8Helper.charset); + } + public static String readTextFile(String... file) { byte[] data = readFile(file); if (data.length > 1 && data[0] == (byte) 0xef && data[1] == (byte) 0xbb && data[2] == (byte) 0xbf) { @@ -983,13 +1018,11 @@ public class Helper { writer.appendNoHilight(" "); } writer.appendNoHilight(byteToHex(data[idx])).appendNoHilight(" "); - } else { - if (addChars) { - if (i > 0 && i % groupSize == 0) { - writer.appendNoHilight(" "); - } - writer.appendNoHilight(" "); + } else if (addChars) { + if (i > 0 && i % groupSize == 0) { + writer.appendNoHilight(" "); } + writer.appendNoHilight(" "); } address += bytesPerRow; } diff --git a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index 1a38454d0..2e6b00c48 100644 --- a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -43,6 +43,12 @@ import com.jpexs.decompiler.flash.abc.types.traits.Trait; import com.jpexs.decompiler.flash.action.parser.ActionParseException; import com.jpexs.decompiler.flash.action.parser.pcode.ASMParser; import com.jpexs.decompiler.flash.action.parser.script.ActionScript2Parser; +import com.jpexs.decompiler.flash.amf.amf3.Amf3InputStream; +import com.jpexs.decompiler.flash.amf.amf3.Amf3OutputStream; +import com.jpexs.decompiler.flash.amf.amf3.Amf3Value; +import com.jpexs.decompiler.flash.amf.amf3.NoSerializerExistsException; +import com.jpexs.decompiler.flash.amf.amf3.Traits; +import com.jpexs.decompiler.flash.amf.amf3.types.ObjectType; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.configuration.ConfigurationItem; import com.jpexs.decompiler.flash.docs.As3PCodeDocs; @@ -55,6 +61,7 @@ import com.jpexs.decompiler.flash.exporters.MovieExporter; import com.jpexs.decompiler.flash.exporters.ShapeExporter; import com.jpexs.decompiler.flash.exporters.SoundExporter; import com.jpexs.decompiler.flash.exporters.TextExporter; +import com.jpexs.decompiler.flash.exporters.amf.amf3.Amf3Exporter; import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; import com.jpexs.decompiler.flash.exporters.modes.ButtonExportMode; @@ -98,6 +105,8 @@ import com.jpexs.decompiler.flash.importers.MorphShapeImporter; import com.jpexs.decompiler.flash.importers.ShapeImporter; import com.jpexs.decompiler.flash.importers.SwfXmlImporter; import com.jpexs.decompiler.flash.importers.TextImporter; +import com.jpexs.decompiler.flash.importers.amf.amf3.Amf3Importer; +import com.jpexs.decompiler.flash.importers.amf.amf3.Amf3ParseException; import com.jpexs.decompiler.flash.tags.DefineBinaryDataTag; import com.jpexs.decompiler.flash.tags.DefineBitsJPEG2Tag; import com.jpexs.decompiler.flash.tags.DefineBitsJPEG3Tag; @@ -105,6 +114,7 @@ import com.jpexs.decompiler.flash.tags.DefineBitsJPEG4Tag; import com.jpexs.decompiler.flash.tags.DefineSpriteTag; import com.jpexs.decompiler.flash.tags.FileAttributesTag; import com.jpexs.decompiler.flash.tags.JPEGTablesTag; +import com.jpexs.decompiler.flash.tags.PlaceObject4Tag; import com.jpexs.decompiler.flash.tags.ScriptLimitsTag; import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag; import com.jpexs.decompiler.flash.tags.Tag; @@ -116,11 +126,14 @@ import com.jpexs.decompiler.flash.tags.base.FontTag; import com.jpexs.decompiler.flash.tags.base.ImageTag; import com.jpexs.decompiler.flash.tags.base.MissingCharacterHandler; import com.jpexs.decompiler.flash.tags.base.MorphShapeTag; +import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag; import com.jpexs.decompiler.flash.tags.base.ShapeTag; import com.jpexs.decompiler.flash.tags.base.SoundTag; import com.jpexs.decompiler.flash.tags.base.TextImportErrorHandler; import com.jpexs.decompiler.flash.tags.base.TextTag; +import com.jpexs.decompiler.flash.timeline.Timelined; import com.jpexs.decompiler.flash.treeitems.SWFList; +import com.jpexs.decompiler.flash.types.CXFORMWITHALPHA; import com.jpexs.decompiler.flash.types.RECT; import com.jpexs.decompiler.flash.types.sound.SoundFormat; import com.jpexs.decompiler.flash.xfl.FLAVersion; @@ -129,6 +142,7 @@ import com.jpexs.decompiler.graph.CompilationException; import com.jpexs.decompiler.graph.DottedChain; import com.jpexs.helpers.CancellableWorker; import com.jpexs.helpers.Helper; +import com.jpexs.helpers.MemoryInputStream; import com.jpexs.helpers.Path; import com.jpexs.helpers.ProgressListener; import com.jpexs.helpers.stat.StatisticData; @@ -142,6 +156,7 @@ import com.sun.jna.platform.win32.Kernel32; import gnu.jpdf.PDFJob; import java.awt.Color; import java.awt.Graphics; +import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Paper; @@ -165,10 +180,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.Stack; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -192,6 +209,9 @@ public class CommandLineArgumentParser { private static String stdErr = null; + private static final String METADATA_FORMAT_JSLIKE = "jslike"; + private static final String METADATA_FORMAT_RAW = "raw"; + @SuppressWarnings("unchecked") private static final ConfigurationItem[] commandlineConfigBoolean = new ConfigurationItem[]{ Configuration.decompile, @@ -523,6 +543,40 @@ public class CommandLineArgumentParser { out.println(" ... is currently only html"); } + if (filter == null || filter.equals("getinstancemetadata")) { + out.println(" " + (cnt++) + ") -getInstanceMetadata -instance [-outputFormat ] [-key ] [-datafile ] "); + out.println(" ...reads instance metadata"); + out.println(" ...-instance : name of instance to fetch metadata from"); + out.println(" ...-outputFormat (optional): format of output - one of: jslike|raw. Default is jslike."); + out.println(" ...- key (optional): name of subkey to display. When present, only value from subkey is shown, whole object value otherwise."); + out.println(" ...-datafile (optional): File to write the data to. If ommited, stdout is used."); + out.println(" ...: SWF file to read metadata from"); + } + + if (filter == null || filter.equals("setinstancemetadata")) { + out.println(" " + (cnt++) + ") -setInstanceMetadata -instance [-inputFormat ] [-key ] [-value | -datafile ] [-outfile ] "); + out.println(" ...adds metadata to instance"); + out.println(" ...-instance : name of instance to replace data in"); + out.println(" ...-inputFormat : format of input data - one of: jslike|raw. Default is jslike."); + out.println(" ...- key (optional): name of subkey to use. When present, the value is set as object property with the name."); + out.println(" Otherwise the value is set directly to the instance without any subkeys."); + out.println(" ...-value (optional): value to set."); + out.println(" ...-datafile (optional): value to set from file."); + out.println(" ...If no -value or -infile parameter present, the value to set is taken from stdin."); + out.println(" ...-outfile (optional): Where to save resulting file. If ommited, original SWF file is overwritten."); + out.println(" ...: SWF file to search instance in"); + } + + if (filter == null || filter.equals("removeinstancemetadata")) { + out.println(" " + (cnt++) + ") -removeInstanceMetadata -instance [-key ] [-outfile ] "); + out.println(" ...removes metadata from instance"); + out.println(" ...-instance : name of instance to remove data from"); + out.println(" ...- key (optional): name of subkey to remove. When present, only the value from subkey of the AMF object is removed."); + out.println(" Otherwise all metadata are removed from the instance."); + out.println(" ...-outfile (optional): Where to save resulting file. If ommited, original SWF file is overwritten."); + out.println(" ...: SWF file to search instance in"); + } + printCmdLineUsageExamples(out, filter); } @@ -530,68 +584,87 @@ public class CommandLineArgumentParser { out.println(); out.println("Examples:"); + final String PREFIX = "java -jar ffdec.jar "; + boolean exampleFound = false; if (filter == null) { - out.println("java -jar ffdec.jar myfile.swf"); + out.println(PREFIX + "myfile.swf"); exampleFound = true; } if (filter == null || filter.equals("proxy")) { - out.println("java -jar ffdec.jar -proxy"); - out.println("java -jar ffdec.jar -proxy -P1234"); + out.println(PREFIX + "-proxy"); + out.println(PREFIX + "-proxy -P1234"); exampleFound = true; } if (filter == null || filter.equals("export") || filter.equals("format") || filter.equals("selectclass") || filter.equals("onerror")) { - out.println("java -jar ffdec.jar -export script \"C:\\decompiled\" myfile.swf"); - out.println("java -jar ffdec.jar -selectclass com.example.MyClass,com.example.SecondClass -export script \"C:\\decompiled\" myfile.swf"); - out.println("java -jar ffdec.jar -format script:pcode -export script \"C:\\decompiled\" myfile.swf"); - out.println("java -jar ffdec.jar -format script:pcode,text:plain -export script,text,image \"C:\\decompiled\" myfile.swf"); - out.println("java -jar ffdec.jar -format fla:cs5.5 -export fla \"C:\\sources\\myfile.fla\" myfile.swf"); - out.println("java -jar ffdec.jar -onerror ignore -export script \"C:\\decompiled\" myfile.swf"); - out.println("java -jar ffdec.jar -onerror retry 5 -export script \"C:\\decompiled\" myfile.swf"); + out.println(PREFIX + "-export script \"C:\\decompiled\" myfile.swf"); + out.println(PREFIX + "-selectclass com.example.MyClass,com.example.SecondClass -export script \"C:\\decompiled\" myfile.swf"); + out.println(PREFIX + "-format script:pcode -export script \"C:\\decompiled\" myfile.swf"); + out.println(PREFIX + "-format script:pcode,text:plain -export script,text,image \"C:\\decompiled\" myfile.swf"); + out.println(PREFIX + "-format fla:cs5.5 -export fla \"C:\\sources\\myfile.fla\" myfile.swf"); + out.println(PREFIX + "-onerror ignore -export script \"C:\\decompiled\" myfile.swf"); + out.println(PREFIX + "-onerror retry 5 -export script \"C:\\decompiled\" myfile.swf"); exampleFound = true; } if (filter == null || filter.equals("cli")) { - out.println("java -jar ffdec.jar -cli myfile.swf"); + out.println(PREFIX + "-cli myfile.swf"); exampleFound = true; } if (filter == null || filter.equals("dumpswf")) { - out.println("java -jar ffdec.jar -dumpSWF myfile.swf"); + out.println(PREFIX + "-dumpSWF myfile.swf"); exampleFound = true; } if (filter == null || filter.equals("compress")) { - out.println("java -jar ffdec.jar -compress myfile.swf myfilecomp.swf"); + out.println(PREFIX + "-compress myfile.swf myfilecomp.swf"); exampleFound = true; } if (filter == null || filter.equals("decompress")) { - out.println("java -jar ffdec.jar -decompress myfile.swf myfiledec.swf"); + out.println(PREFIX + "-decompress myfile.swf myfiledec.swf"); exampleFound = true; } if (filter == null || filter.equals("config")) { - out.println("java -jar ffdec.jar -config autoDeobfuscate=1,parallelSpeedUp=0 -export script \"C:\\decompiled\" myfile.swf"); + out.println(PREFIX + "-config autoDeobfuscate=1,parallelSpeedUp=0 -export script \"C:\\decompiled\" myfile.swf"); exampleFound = true; } if (filter == null || filter.equals("deobfuscate")) { - out.println("java -jar ffdec.jar -deobfuscate max myas3file_secure.swf myas3file.swf"); + out.println(PREFIX + "-deobfuscate max myas3file_secure.swf myas3file.swf"); exampleFound = true; } if (filter == null || filter.equals("enabledebugging")) { - out.println("java -jar ffdec.jar -enabledebugging -injectas3 myas3file.swf myas3file_debug.swf"); - out.println("java -jar ffdec.jar -enabledebugging -generateswd myas2file.swf myas2file_debug.swf"); + out.println(PREFIX + "-enabledebugging -injectas3 myas3file.swf myas3file_debug.swf"); + out.println(PREFIX + "-enabledebugging -generateswd myas2file.swf myas2file_debug.swf"); exampleFound = true; } if (filter == null || filter.equals("doc")) { - out.println("java -jar ffdec.jar -doc -type as3.pcode.instructions -format html"); - out.println("java -jar ffdec.jar -doc -type as3.pcode.instructions -format html -locale en -out as3_docs_en.html"); + out.println(PREFIX + "-doc -type as3.pcode.instructions -format html"); + out.println(PREFIX + "-doc -type as3.pcode.instructions -format html -locale en -out as3_docs_en.html"); + exampleFound = true; + } + + if (filter == null || filter.equals("getinstancemetadata")) { + out.println(PREFIX + "-getInstanceMetadata -instance myobj -key keyone myfile.swf"); + out.println(PREFIX + "-getInstanceMetadata -instance myobj2 -outputFormat raw -outfile out.amf myfile.swf"); + exampleFound = true; + } + if (filter == null || filter.equals("setinstancemetadata")) { + out.println(PREFIX + "-setInstanceMetadata -instance myobj -key mykey -value 1234 myfile.swf"); + out.println(PREFIX + "-setInstanceMetadata -instance myobj -key my -inputFormat raw -datafile value.amf -outfile modified.swf myfile.swf"); + exampleFound = true; + } + + if (filter == null || filter.equals("removeinstancemetadata")) { + out.println(PREFIX + "-removeInstanceMetadata -instance myobj -key mykey -outfile result.swf myfile.swf"); + out.println(PREFIX + "-removeInstanceMetadata -instance myobj myfile.swf"); exampleFound = true; } @@ -634,6 +707,9 @@ public class CommandLineArgumentParser { if (nextParamOriginal != null) { nextParam = nextParamOriginal.toLowerCase(); } + if (nextParam == null) { + nextParam = ""; + } switch (nextParam) { case "-cli": cliMode = true; @@ -709,11 +785,20 @@ public class CommandLineArgumentParser { } String command = ""; + if (nextParam == null) { + nextParam = ""; + } if (nextParam.startsWith("-")) { command = nextParam.substring(1); } - if (command.equals("removefromcontextmenu")) { + if (command.equals("getinstancemetadata")) { + parseGetInstanceMetadata(args); + } else if (command.equals("setinstancemetadata")) { + parseSetInstanceMetadata(args); + } else if (command.equals("removeinstancemetadata")) { + parseRemoveInstanceMetadata(args); + } else if (command.equals("removefromcontextmenu")) { if (!args.isEmpty()) { badArguments(command); } @@ -902,6 +987,492 @@ public class CommandLineArgumentParser { setConfigurations(args.pop()); } + private static void parseGetInstanceMetadata(Stack args) { + if (args.size() < 3) { + badArguments("getinstancemetadata"); + } + Set processedParams = new HashSet<>(); + String format = METADATA_FORMAT_JSLIKE; + String key = null; + String instance = null; + File stdOutFile = null; + File swfFile = null; + + while (!args.empty()) { + String paramName = args.pop().toLowerCase(); + if (processedParams.contains(paramName)) { + System.err.println("Parameter " + paramName + " can appear only once."); + } + switch (paramName) { + case "-instance": + if (args.isEmpty()) { + System.err.println("Missing instance name"); + badArguments("getinstancemetadata"); + } + instance = args.pop(); + break; + case "-outputformat": + if (args.empty()) { + System.err.println("Missing format value"); + badArguments("getinstancemetadata"); + } + format = args.pop(); + if (!Arrays.asList(METADATA_FORMAT_RAW, METADATA_FORMAT_JSLIKE).contains(format)) { + System.err.println("Invalid output format"); + badArguments("getinstancemetadata"); + } + break; + case "-key": + if (args.empty()) { + System.err.println("Missing key value"); + badArguments("getinstancemetadata"); + } + + key = args.pop(); + break; + case "-datafile": + if (args.empty()) { + System.err.println("Missing datafile file"); + badArguments("getinstancemetadata"); + } + stdOutFile = new File(args.pop()); + break; + default: + if (!args.isEmpty()) { + badArguments("getinstancemetadata"); + } + swfFile = new File(paramName); + paramName = null; + } + if (paramName != null) { + processedParams.add(paramName); + } + } + if (instance == null) { + System.err.println("No instance specified"); + badArguments("getinstancemetadata"); + } + if (swfFile == null) { + System.err.println("No SWF file specified"); + badArguments("getinstancemetadata"); + } + + final String fInstance = instance; + final String fKey = key; + final String fFormat = format; + + processReadSWF(swfFile, stdOutFile, new SwfAction() { + @Override + public void swfAction(SWF swf, OutputStream stdout) throws IOException { + if (!processTimelined(swf, stdout)) { + System.err.println("No instance with name " + fInstance + " found"); + System.exit(0); + } + } + + private boolean processTimelined(Timelined tim, OutputStream stdout) throws IOException { + ReadOnlyTagList rtl = tim.getTags(); + for (int i = 0; i < rtl.size(); i++) { + Tag t = rtl.get(i); + if (t instanceof Timelined) { + if (processTimelined((Timelined) t, stdout)) { + return true; + } + } + if (t instanceof PlaceObjectTypeTag) { + PlaceObjectTypeTag pt = (PlaceObjectTypeTag) t; + String instanceName = pt.getInstanceName(); + if (fInstance.equals(instanceName)) { + Amf3Value oldValue = pt.getAmfData(); + if (oldValue == null) { + System.err.println("No metadata for instance " + instanceName + " found"); + System.exit(1); //TODO? Different exit code + } + Object actualValue = oldValue.getValue(); + + Object displayVal = actualValue; + if (fKey != null) { + if (actualValue instanceof ObjectType) { + ObjectType ot = (ObjectType) actualValue; + if (ot.containsDynamicMember(fKey)) { + displayVal = ot.getDynamicMember(fKey); + } else { + System.err.println("No value with key " + fKey + " exists"); + System.err.println("Available keys: " + String.join(",", ot.dynamicMembersKeySet())); + System.exit(1); + } + } else { + System.err.println("Metadata present, but not as Object type, cannot get key " + fKey); + System.exit(1); + } + } + + switch (fFormat) { + case METADATA_FORMAT_JSLIKE: + stdout.write(Utf8Helper.getBytes(Amf3Exporter.amfToString(displayVal, " ", System.lineSeparator()) + System.lineSeparator())); + break; + case METADATA_FORMAT_RAW: + Amf3OutputStream aos = new Amf3OutputStream(stdout); + try { + aos.writeValue(displayVal); + } catch (NoSerializerExistsException ex) { + //should not happen + } + break; + } + return true; + } + } + } + return false; + } + + }); + System.exit(0); + } + + private static void parseSetInstanceMetadata(Stack args) { + if (args.size() < 3) { + badArguments("setinstancemetadata"); + } + Set processedParams = new HashSet<>(); + String format = METADATA_FORMAT_JSLIKE; + String key = null; + String instance = null; + File outFile = null; + File swfFile = null; + String value = null; + File valueFile = null; + + while (!args.empty()) { + String paramName = args.pop().toLowerCase(); + if (processedParams.contains(paramName)) { + System.err.println("Parameter " + paramName + " can appear only once."); + } + switch (paramName) { + case "-instance": + if (args.isEmpty()) { + System.err.println("Missing instance name"); + badArguments("setinstancemetadata"); + } + instance = args.pop(); + break; + case "-inputformat": + if (args.empty()) { + System.err.println("Missing format value"); + badArguments("setinstancemetadata"); + } + format = args.pop(); + if (!Arrays.asList(METADATA_FORMAT_RAW, METADATA_FORMAT_JSLIKE).contains(format)) { + System.err.println("Invalid output format"); + badArguments("setinstancemetadata"); + } + break; + case "-key": + if (args.empty()) { + System.err.println("Missing key value"); + badArguments("setinstancemetadata"); + } + + key = args.pop(); + break; + case "-value": + if (args.empty()) { + System.err.println("Missing value"); + badArguments("setinstancemetadata"); + } + + value = args.pop(); + break; + case "-outfile": + if (args.empty()) { + System.err.println("Missing outFile"); + badArguments("setinstancemetadata"); + } + outFile = new File(args.pop()); + break; + + case "-datafile": + if (args.empty()) { + System.err.println("Missing datafile file"); + badArguments("setinstancemetadata"); + } + valueFile = new File(args.pop()); + break; + default: + if (!args.isEmpty()) { + badArguments("setinstancemetadata"); + } + swfFile = new File(paramName); + paramName = null; + } + if (paramName != null) { + processedParams.add(paramName); + } + } + if (instance == null) { + System.err.println("No instance specified"); + badArguments("getinstancemetadata"); + } + if (swfFile == null) { + System.err.println("No SWF file specified"); + badArguments("getinstancemetadata"); + } + if (outFile == null) { + outFile = swfFile; + } + + byte[] valueBytes = new byte[]{}; + if (valueFile != null) { + try { + valueBytes = Helper.readFileEx(valueFile.getAbsolutePath()); + } catch (IOException ex) { + System.err.println("Cannot read value: " + ex.getMessage()); + System.exit(1); + return; + } + } else if (value != null) { + valueBytes = Utf8Helper.getBytes(value); + } + + if (valueBytes.length == 0) { + valueBytes = Helper.readStream(System.in); + } + + if (valueBytes.length < 1) { + System.err.println("No value to set specified"); + System.exit(1); + } + + Object amfValue = null; + try { + switch (format) { + case METADATA_FORMAT_JSLIKE: + Amf3Importer importer = new Amf3Importer(); + amfValue = importer.stringToAmf(value); + break; + case METADATA_FORMAT_RAW: + Amf3InputStream ais = new Amf3InputStream(new MemoryInputStream(valueBytes)); + amfValue = ais.readValue("val"); + break; + } + } catch (IOException | Amf3ParseException | NoSerializerExistsException ex) { + System.err.println("Error parsing input value: " + ex.getMessage()); + System.exit(1); + return; + } + + final String fInstance = instance; + final String fKey = key; + final Object fAmfValue = amfValue; + + processModifySWF(swfFile, outFile, null, new SwfAction() { + @Override + public void swfAction(SWF swf, OutputStream stdout) throws IOException { + if (!processTimelined(swf, stdout)) { + System.err.println("No instance with name " + fInstance + " found"); + System.exit(0); + } + } + + private boolean processTimelined(Timelined tim, OutputStream stdout) throws IOException { + ReadOnlyTagList rtl = tim.getTags(); + for (int i = 0; i < rtl.size(); i++) { + Tag t = rtl.get(i); + if (t instanceof Timelined) { + if (processTimelined((Timelined) t, stdout)) { + return true; + } + } + if (t instanceof PlaceObjectTypeTag) { + PlaceObjectTypeTag pt = (PlaceObjectTypeTag) t; + String instanceName = pt.getInstanceName(); + if (fInstance.equals(instanceName)) { + + Amf3Value oldValue = pt.getAmfData(); + if (oldValue != null && oldValue.getValue() == null) { + oldValue = null; + } + if (oldValue != null && fKey != null) { //it has AMFData and we are going to set key + Object actualValue = oldValue.getValue(); + if (actualValue instanceof ObjectType) { //add it to ObjectType + ObjectType ot = (ObjectType) actualValue; + ot.putDynamicMember(fKey, fAmfValue); + t.setModified(true); + oldValue.setValue(ot); + System.out.println("Key " + fKey + " added"); + System.out.println("New instance data for " + instanceName + ":"); + System.out.println(Amf3Exporter.amfToString(ot, " ", System.lineSeparator())); + return true; + } + } + + PlaceObject4Tag pt4; + if (pt instanceof PlaceObject4Tag) { + pt4 = (PlaceObject4Tag) pt; + } else { + pt4 = new PlaceObject4Tag( + pt.getSwf(), pt.flagMove(), pt.getDepth(), pt.getClassName(), pt.getCharacterId(), pt.getMatrix(), pt.getColorTransform() == null ? null : new CXFORMWITHALPHA(pt.getColorTransform()), pt.getRatio(), + pt.getInstanceName(), pt.getClipDepth(), pt.getFilters(), pt.getBlendMode(), pt.getBitmapCache(), pt.getVisible(), pt.getBackgroundColor(), pt.getClipActions(), pt.getAmfData()); + tim.replaceTag(i, pt4); + } + + Object newValue; + if (fKey != null) { + ObjectType ot = new ObjectType(new Traits("", true, new ArrayList<>())); + ot.put(fKey, fAmfValue); + newValue = ot; + } else { + newValue = fAmfValue; + } + pt4.amfData = new Amf3Value(newValue); + pt4.setModified(true); + + System.out.println("New instance data for " + instanceName + ":"); + System.out.println(Amf3Exporter.amfToString(newValue, " ", System.lineSeparator())); + + return true; + } + } + } + return false; + } + + }); + System.exit(0); + } + + private static void parseRemoveInstanceMetadata(Stack args) { + if (args.size() < 2) { + badArguments("removeinstancemetadata"); + } + + Set processedParams = new HashSet<>(); + String key = null; + String instance = null; + File swfFile = null; + File outFile = null; + while (!args.empty()) { + String paramName = args.pop().toLowerCase(); + if (processedParams.contains(paramName)) { + System.err.println("Parameter " + paramName + " can appear only once."); + } + switch (paramName) { + case "-instance": + if (args.isEmpty()) { + System.err.println("Missing instance name"); + badArguments("removeinstancemetadata"); + } + instance = args.pop(); + break; + case "-key": + if (args.empty()) { + System.err.println("Missing key value"); + badArguments("removeinstancemetadata"); + } + + key = args.pop(); + break; + case "-outfile": + if (args.empty()) { + System.err.println("Missing outFile"); + badArguments("removeinstancemetadata"); + } + outFile = new File(args.pop()); + break; + default: + if (!args.isEmpty()) { + badArguments("removeinstancemetadata"); + } + swfFile = new File(paramName); + paramName = null; + } + if (paramName != null) { + processedParams.add(paramName); + } + } + if (instance == null) { + System.err.println("No instance specified"); + badArguments("removeinstancemetadata"); + } + if (swfFile == null) { + System.err.println("No SWF file specified"); + badArguments("removeinstancemetadata"); + } + if (outFile == null) { + outFile = swfFile; + } + + final String fInstance = instance; + final String fKey = key; + + processModifySWF(swfFile, outFile, null, new SwfAction() { + @Override + public void swfAction(SWF swf, OutputStream stdout) throws IOException { + if (!processTimelined(swf, stdout)) { + System.err.println("No instance with name " + fInstance + " found"); + System.exit(0); + } + } + + private boolean processTimelined(Timelined tim, OutputStream stdout) throws IOException { + ReadOnlyTagList rtl = tim.getTags(); + for (int i = 0; i < rtl.size(); i++) { + Tag t = rtl.get(i); + if (t instanceof Timelined) { + if (processTimelined((Timelined) t, stdout)) { + return true; + } + } + if (t instanceof PlaceObject4Tag) { + PlaceObject4Tag pt4 = (PlaceObject4Tag) t; + String instanceName = pt4.getInstanceName(); + if (fInstance.equals(instanceName)) { + Amf3Value oldValue = pt4.getAmfData(); + if (oldValue == null) { + System.err.println("No metadata for instance " + instanceName + " found"); + System.exit(1); //TODO? Different exit code + } + Object actualValue = oldValue.getValue(); + + if (fKey != null) { + if (actualValue instanceof ObjectType) { + ObjectType ot = (ObjectType) actualValue; + if (ot.containsDynamicMember(fKey)) { + ot.remove(fKey); + oldValue.setValue(ot); + System.out.println("Key " + fKey + " removed"); + System.out.println("New instance data for " + instanceName + ":"); + System.out.println(Amf3Exporter.amfToString(ot, " ", System.lineSeparator())); + pt4.setModified(true); + return true; + } else { + System.err.println("No value with key " + fKey + " exists"); + System.err.println("Available keys: " + String.join(",", ot.dynamicMembersKeySet())); + System.exit(1); + } + } else { + System.err.println("Metadata present, but not as Object type, cannot remove key " + fKey); + System.exit(1); + } + } else { + pt4.amfData = null; + pt4.setModified(true); + System.out.println("Whole metadata removed for instance " + instanceName); + } + + return true; + } + } + } + return false; + } + + }); + System.exit(0); + + } + private static class Range { public Integer min; @@ -1665,7 +2236,7 @@ public class CommandLineArgumentParser { badArguments("deobfuscate"); } String mode = args.pop(); - DeobfuscationLevel lev = null; + DeobfuscationLevel lev; switch (mode) { case "controlflow": case "max": @@ -1683,7 +2254,7 @@ public class CommandLineArgumentParser { default: System.err.println("Invalid level, must be one of: controlflow,traps,deadcode or 1,2,3/max"); System.exit(1); - break; + return; } File inFile = new File(args.pop()); File outFile = new File(args.pop()); @@ -2112,9 +2683,11 @@ public class CommandLineArgumentParser { Paper p = new Paper(); p.setSize(img.getWidth(), img.getHeight()); pf.setPaper(p); - Graphics g = job.getGraphics(pf); - g.drawImage(img, 0, 0, img.getWidth(), img.getHeight(), null); - g.dispose(); + if (job != null) { + Graphics g = job.getGraphics(pf); + g.drawImage(img, 0, 0, img.getWidth(), img.getHeight(), null); + g.dispose(); + } System.out.println("OK"); } @@ -2445,7 +3018,7 @@ public class CommandLineArgumentParser { List characterIds = new ArrayList<>(); for (int i = 0; i < characterIdsStr.length; i++) { - int characterId = 0; + int characterId; try { characterId = Integer.parseInt(characterIdsStr[i]); characterIds.add(characterId); @@ -2558,12 +3131,13 @@ public class CommandLineArgumentParser { while (true) { String tagNoToRemoveStr = args.pop(); - int tagNo = 0; + int tagNo; try { tagNo = Integer.parseInt(tagNoToRemoveStr); } catch (NumberFormatException nfe) { System.err.println("Tag number should be integer"); System.exit(1); + return; } if (tagNo < 0 || tagNo >= swf.getTags().size()) { System.err.println("Tag number does not exist. Tag number should be between 0 and " + (swf.getTags().size() - 1)); @@ -2883,7 +3457,7 @@ public class CommandLineArgumentParser { } private static void parseInfo(Stack args) throws FileNotFoundException { - File out = null; + File out; PrintWriter pw = new PrintWriter(System.out); boolean found = false; while (!args.isEmpty()) { @@ -2902,7 +3476,7 @@ public class CommandLineArgumentParser { } break; default: - SWFBundle bundle = null; + SWFBundle bundle; String sfile = a; File file = new File(sfile); try { @@ -3208,4 +3782,124 @@ public class CommandLineArgumentParser { } return vals[0]; } + + private static interface SwfAction { + + public void swfAction(SWF swf, OutputStream stdout) throws IOException; + } + + private static void processReadSWF(File inFile, File stdOutFile, SwfAction action) { + OutputStream stdout = null; + + try { + if (stdOutFile != null) { + try { + stdout = new FileOutputStream(stdOutFile); + } catch (FileNotFoundException ex) { + System.err.println("File not found: " + ex.getMessage()); + System.exit(1); + } + } else { + stdout = System.out; + } + + try (FileInputStream is = new FileInputStream(inFile)) { + SWF swf = new SWF(is, Configuration.parallelSpeedUp.get()); + action.swfAction(swf, stdout); + } catch (FileNotFoundException ex) { + System.err.println("File not found: " + ex.getMessage()); + System.exit(1); + } catch (InterruptedException ex) { + logger.log(Level.SEVERE, null, ex); + System.exit(1); + } catch (IOException ex) { + logger.log(Level.SEVERE, "Error", ex); + System.exit(1); + } + } finally { + if (stdOutFile != null) { + if (stdout != null) { + try { + stdout.close(); + } catch (IOException ex) { + //ignore + } + } + } + } + } + + private static void processModifySWF(File inFile, File outFile, File stdOutFile, SwfAction action) { + + OutputStream stdout = null; + + try { + if (stdOutFile != null) { + try { + stdout = new FileOutputStream(stdOutFile); + } catch (FileNotFoundException ex) { + System.err.println("File not found: " + ex.getMessage()); + System.exit(1); + } + } else { + stdout = System.out; + } + + File tmpFile = null; + if (inFile.equals(outFile)) { + try { + tmpFile = File.createTempFile("ffdec_modify_", ".swf"); + outFile = tmpFile; + } catch (IOException ex) { + System.err.println("Unable to create temp file"); + System.exit(1); + } + } + try (FileInputStream is = new FileInputStream(inFile); + FileOutputStream fos = new FileOutputStream(outFile)) { + SWF swf = new SWF(is, Configuration.parallelSpeedUp.get()); + action.swfAction(swf, stdout); + swf.saveTo(fos); + } catch (FileNotFoundException ex) { + System.err.println("File not found: " + ex.getMessage()); + System.exit(1); + } catch (InterruptedException ex) { + logger.log(Level.SEVERE, null, ex); + System.exit(1); + } catch (IOException ex) { + logger.log(Level.SEVERE, "Error", ex); + System.exit(1); + } + + if (tmpFile != null) { + try { + if (!inFile.delete()) { + System.err.println("Cannot overwrite original file"); + System.exit(1); + } + if (!tmpFile.renameTo(inFile)) { + System.err.println("Cannot rename tempfile to original file"); + System.exit(1); + } + tmpFile = null; + System.out.println(inFile + " overwritten."); + } finally { + if (tmpFile != null && tmpFile.exists()) { + tmpFile.delete(); + } + } + } + } finally { + if (stdOutFile != null) { + if (stdout != null) { + try { + stdout.close(); + } catch (IOException ex) { + //ignore + } + } + } + } + } + } diff --git a/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/src/com/jpexs/decompiler/flash/gui/MainPanel.java index 3b373bc09..ce5577f9e 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -3592,6 +3592,10 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se @Override public void addTag(int index, Tag tag) { } + + @Override + public void replaceTag(int index, Tag newTag) { + } }; }