package com.intellij.flex.uiDesigner.libraries; import com.intellij.flex.uiDesigner.abc.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.vfs.VirtualFile; import gnu.trove.TIntObjectHashMap; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; class AbcMerger extends AbcTranscoder { private static final Logger LOG = Logger.getInstance(AbcMerger.class.getName()); private final Map<CharSequence, Definition> definitionMap; private final FileOutputStream out; // old id to new private final TIntObjectHashMap<SymbolInfo> currentSymbolsInfo = new TIntObjectHashMap<>(); private final ArrayList<SymbolInfo> symbols = new ArrayList<>(); private int symbolCounter; @Nullable private Library library; @Nullable private DefinitionProcessor definitionProcessor; public AbcMerger(Map<CharSequence, Definition> definitionMap, File outFile, @Nullable DefinitionProcessor definitionProcessor) throws IOException { this.definitionMap = definitionMap; this.definitionProcessor = definitionProcessor; //noinspection IOResourceOpenedButNotSafelyClosed out = new FileOutputStream(outFile); channel = out.getChannel(); channel.position(SwfUtil.getWrapHeaderLength()); } public void setDefinitionProcessor(@Nullable DefinitionProcessor definitionProcessor) { this.definitionProcessor = definitionProcessor; } @Override protected void readFrameSizeFrameRateAndFrameCount(byte b) throws IOException { super.readFrameSizeFrameRateAndFrameCount(b); lastWrittenPosition = buffer.position(); } public void process(Library library) throws IOException { this.library = library; VirtualFile file = library.getSwfFile(); process(file.getInputStream(), (int)file.getLength()); } public void process(InputStream in) throws IOException { process(in, in.available()); } public void process(InputStream in, int length) throws IOException { readSource(in, length); processTags(null); library = null; if (!currentSymbolsInfo.isEmpty()) { symbols.ensureCapacity(symbols.size() + currentSymbolsInfo.size()); currentSymbolsInfo.forEachValue(info -> { // not all objects with character id are exported if (info.start == -1) { assert info.end == -1; } else { symbols.add(info); } return true; }); currentSymbolsInfo.clear(); } } public void end(List<Decoder> decoders, Encoder encoder) throws IOException { encoder.configure(decoders, null); SwfUtil.mergeDoAbc(decoders, encoder); encoder.writeDoAbc(channel, true); int length = 0; for (SymbolInfo info : symbols) { length += 2 + (info.end - info.start); } Collections.sort(symbols, (o1, o2) -> o1.newId - o2.newId); buffer.clear(); encodeTagHeader(TagTypes.SymbolClass, length + 2); buffer.putShort((short)symbols.size()); buffer.flip(); channel.write(buffer); buffer.clear(); for (SymbolInfo info : symbols) { final ByteBuffer b = info.buffer; final int start = info.start - 2; b.putShort(start, (short)info.newId); b.position(start); b.limit(info.end); channel.write(b); b.limit(b.capacity()); } // write footer - ShowFrame and End buffer.clear(); SwfUtil.footer(buffer); buffer.flip(); channel.write(buffer); SwfUtil.header(channel, buffer); } public void close() throws IOException { if (out != null) { out.close(); } channel = null; } @Override protected int processTag(int type, int length) throws IOException { if (super.processTag(type, length) == 1 || type == TagTypes.FileAttributes || type == TagTypes.ShowFrame) { return 1; } int characterIdPosition = -1; switch (type) { case TagTypes.DefineScalingGrid: case TagTypes.DefineSprite: case TagTypes.DefineShape: case TagTypes.DefineShape2: case TagTypes.DefineShape3: case TagTypes.DefineShape4: case TagTypes.DefineBinaryData: case TagTypes.DefineBitsLossless2: case TagTypes.DefineBitsLossless: case TagTypes.DefineBitsJPEG2: case TagTypes.DefineBitsJPEG3: case TagTypes.DefineBitsJPEG4: case TagTypes.DefineBits: case TagTypes.PlaceObject: case TagTypes.DefineFont: case TagTypes.DefineFont2: case TagTypes.DefineFont3: case TagTypes.DefineText: case TagTypes.DefineEditText: case TagTypes.DefineButton: case TagTypes.DefineButton2: case TagTypes.DefineMorphShape: case TagTypes.DefineMorphShape2: // character id after header characterIdPosition = buffer.position(); break; case TagTypes.PlaceObject2: case TagTypes.PlaceObject3: if (updatePlaceObject2Or3Reference(type == TagTypes.PlaceObject2)) { return 0; } break; case TagTypes.DefineFontName: case TagTypes.DefineFontAlignZones: case TagTypes.DefineFontInfo: updateReferenceById(buffer.position()); return 0; } if (characterIdPosition != -1) { changeCharacterId(characterIdPosition); if (type == TagTypes.DefineSprite) { processDefineSprite(length); } } return 0; } private boolean updatePlaceObject2Or3Reference(boolean is2) { if ((buffer.get(buffer.position()) & PlaceObjectFlags.HAS_CHARACTER) == 0) { return false; } int bufferPosition; if (is2) { bufferPosition = buffer.position() + 3; } else { bufferPosition = buffer.position() + 4; final byte flags2 = buffer.get(buffer.position() + 1); if ((flags2 & PlaceObject3Flags.HAS_CLASS_NAME) != 0 || (flags2 & PlaceObject3Flags.HAS_IMAGE) != 0) { bufferPosition += skipAbcName(bufferPosition) + 1; } } updateReferenceById(bufferPosition); return true; } private void updateReferenceById(int idPosition) { final int oldId = buffer.getShort(idPosition); SymbolInfo info = currentSymbolsInfo.get(oldId); // swf may be invalid if (info != null) { buffer.putShort(idPosition, (short)info.newId); } else { LOG.warn("swf invalid, cannot update reference " + oldId + " due to object with this id is not defined yet"); } } private void changeCharacterId(int idPosition) { final int oldId = buffer.getShort(idPosition); SymbolInfo info = currentSymbolsInfo.get(oldId); // swf may be invalid if (info == null) { info = new SymbolInfo(++symbolCounter, buffer); currentSymbolsInfo.put(oldId, info); } buffer.putShort(idPosition, (short)info.newId); } private void processDefineSprite(int spriteTagLength) { buffer.mark(); buffer.position(buffer.position() + 4); final int endPosition = buffer.position() + spriteTagLength; while (true) { final int tagCodeAndLength = buffer.getShort(); final int type = tagCodeAndLength >> 6; int length = tagCodeAndLength & 0x3F; if (length == 63) { length = buffer.getInt(); } final int start = buffer.position(); switch (type) { case TagTypes.PlaceObject: updateReferenceById(start); break; case TagTypes.PlaceObject2: case TagTypes.PlaceObject3: updatePlaceObject2Or3Reference(type == TagTypes.PlaceObject2); break; } final int newPosition = start + length; if (newPosition < endPosition) { buffer.position(newPosition); } else { break; } } buffer.reset(); } @Override protected void ensureExportAssetsStorageCreated(int numSymbols) { } @Override protected void processEnd(int length) throws IOException { skipTag(length); } @Override protected void doAbc2(int length) throws IOException { final Definition definition = definitionMap.get(transientNameString); // may be overloaded (i.e. new definition with high timestamp exists) if (definition != null && (definition.getLibrary() == null || definition.getLibrary().library == library)) { definition.doAbcData = createBufferWrapper(length); if (definitionProcessor != null) { definitionProcessor.process(transientNameString, buffer, definition, definitionMap); } } } @Override protected void storeExportAsset(int id, int start, int end, boolean mainNameRead) { if (id == 0) { if (mainNameRead) { Definition removed = definitionMap.remove(transientNameString); assert removed != null; } return; } SymbolInfo info = currentSymbolsInfo.get(id); if (info == null) { throw new IllegalStateException("info cannot be null, library: " + (library == null ? "<InputStream>" : library.getFile().getPath())); } else { info.start = start; info.end = end; } } @Override protected void processSymbolClass(final int length) throws IOException { processExportAssets(length, true); } private static class SymbolInfo { public int start = -1; public int end = -1; public final int newId; public final ByteBuffer buffer; private SymbolInfo(int newId, ByteBuffer buffer) { this.newId = newId; this.buffer = buffer; } } }