// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.gui.hexview; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.infinity.resource.AbstractStruct; import org.infinity.resource.StructEntry; import org.infinity.util.io.StreamUtils; import tv.porst.jhexview.DataChangedEvent; import tv.porst.jhexview.IDataChangedListener; import tv.porst.jhexview.IDataProvider; /** * Provides data as byte array from the associated AbstractStruct instance to be used in * JHexView components. */ public class StructuredDataProvider implements IDataProvider { private final ArrayList<IDataChangedListener> listeners = new ArrayList<IDataChangedListener>(); private final AbstractStruct struct; private List<StructEntry> listStructures; private int dataSize; /** Constructs a new DataProvider object that can be used in JHexView components. */ public StructuredDataProvider(AbstractStruct struct) { if (struct == null) { throw new NullPointerException("struct is null"); } this.struct = struct; this.dataSize = -1; // mark as uninitialized this.listStructures = null; // mark as uninitialized } //--------------------- Begin Interface IDataProvider --------------------- @Override public void addListener(IDataChangedListener listener) { if (listener != null && listeners.indexOf(listener) < 0) { listeners.add(listener); } } @Override public byte[] getData(long offset, int length) { // checking size int extraLength = 0; if (offset+length > getDataLength()) { extraLength = (int)(offset+length) - getDataLength(); length = getDataLength() - (int)offset; } if (length > 0) { ArrayList<StructEntry> listEntries = new ArrayList<StructEntry>(); int entryIndex = findStructureIndex((int)offset); if (entryIndex >= 0) { // collecting matching entries for (int idx = entryIndex; idx < getCachedList().size(); idx++) { if (getCachedList().get(idx).getOffset() >= offset+length) { break; } listEntries.add(getCachedList().get(idx)); } // creating byte array to return byte[] retVal = new byte[length+extraLength]; // constructing byte array int startOffset = listEntries.get(0).getOffset(); StructEntry entry = listEntries.get(listEntries.size()-1); int fullSize = entry.getOffset()+entry.getSize() - startOffset; ByteArrayOutputStream os = new ByteArrayOutputStream(fullSize); int curOfs = startOffset; for (int idx = 0; idx < listEntries.size(); idx++) { entry = listEntries.get(idx); // sanity check if (entry.getOffset() >= offset+length) { continue; } // filling holes with empty data while (curOfs < entry.getOffset()) { os.write(0); curOfs++; } // writing actual data try { entry.write(os); } catch (IOException e) { e.printStackTrace(); } curOfs += entry.getSize(); } // preparing byte array for output try { System.arraycopy(os.toByteArray(), (int)offset - startOffset, retVal, 0, length); } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } return retVal; } } return null; } @Override public int getDataLength() { if (dataSize < 0) { reset(); } return dataSize; } @Override public boolean hasData(long start, int length) { return (start >= 0 && start+length <= getDataLength()); } @Override public boolean isEditable() { return true; } @Override public boolean keepTrying() { return false; } @Override public void removeListener(IDataChangedListener listener) { if (listener != null) { listeners.remove(listener); } } @Override public void setData(long offset, byte[] data) { if (offset >= 0 && offset < getStruct().getSize() && data != null && data.length > 0) { boolean hasChanged = false; int length = data.length; if (offset+length > getStruct().getSize()) { length = getStruct().getSize() - (int)offset; } if (length > 0) { ArrayList<StructEntry> listEntries = new ArrayList<StructEntry>(); int entryIndex = findStructureIndex((int)offset); // collecting matching entries if (entryIndex >= 0) { int maxEntrySize = 0; // max. possible size of a single entry // collecting matching entries for (int idx = entryIndex; idx < getCachedList().size(); idx++) { if (getCachedList().get(idx).getOffset() >= offset+length) { break; } maxEntrySize = Math.max(maxEntrySize, getCachedList().get(idx).getSize()); listEntries.add(getCachedList().get(idx)); } // we need an output stream to initially load original data from the structure because // of the possibility to write only partial data into a structure. ByteArrayOutputStream os = new ByteArrayOutputStream(maxEntrySize); for (int idx = 0; idx < listEntries.size(); idx++) { StructEntry entry = listEntries.get(idx); os.reset(); try { // pre-initializing byte array with original data entry.write(os); // writing new data into byte array byte[] buffer = os.toByteArray(); int srcOfs = Math.max(0, entry.getOffset() - (int)offset); int dstOfs = Math.max(0, (int)offset - entry.getOffset()); int len = Math.min((int)offset+length, entry.getOffset()+entry.getSize()) - entry.getOffset() - dstOfs; if (len > 0) { System.arraycopy(data, srcOfs, buffer, dstOfs, len); // loading data into the structure entry.read(StreamUtils.getByteBuffer(buffer), 0); hasChanged = true; } } catch (IOException ioe) { ioe.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } } if (hasChanged) { fireDataChanged(); } } } //--------------------- End Interface IDataProvider --------------------- /** Cleans up resources. */ public void close() { if (listStructures != null) { listStructures.clear(); listStructures = null; dataSize = -1; } } /** Re-initializes data cache. */ public void reset() { close(); listStructures = getStruct().getFlatList(); dataSize = 0; for (final StructEntry e: listStructures) { dataSize = Math.max(dataSize, e.getOffset()+e.getSize()); } } /** Returns the attached AbstractStruct instance. */ public AbstractStruct getStruct() { return struct; } /** Returns the StructEntry instance located at the specified offset. */ public StructEntry getFieldAt(int offset) { int index = findStructureIndex(offset); if (index >= 0) { return getCachedList().get(index); } else { return null; } } protected void fireDataChanged() { if (!listeners.isEmpty()) { DataChangedEvent event = new DataChangedEvent(this); for (int i = listeners.size()-1; i >= 0; i--) { listeners.get(i).dataChanged(event); } } } // Returns the list of cached top-level StructEntry objects private List<StructEntry> getCachedList() { if (listStructures == null) { reset(); } return listStructures; } // Returns the list index of the StructEntry containing the specified offset. Returns -1 on failure. private int findStructureIndex(int offset) { StructEntry key = new EmptyStructure(offset); int index = Collections.binarySearch(getCachedList(), key, new Comparator<StructEntry>() { @Override public int compare(StructEntry obj, StructEntry key) { if (key.getOffset() < obj.getOffset()) { return 1; } else if (key.getOffset() >= obj.getOffset()+obj.getSize()) { return -1; } else { return 0; } } }); if (index >= 0 && index < getCachedList().size()) { return index; } else { return -1; } } //-------------------------- INNER CLASSES -------------------------- // A dummy StructEntry implementation that can be used as key in a search operations. private class EmptyStructure implements StructEntry { private int offset; public EmptyStructure(int offset) { this.offset = offset; } @Override public Object clone() { return null; } @Override public int compareTo(StructEntry o) { return 0; } @Override public void write(OutputStream os) throws IOException {} @Override public int read(ByteBuffer buffer, int offset) throws Exception { return offset; } @Override public void copyNameAndOffset(StructEntry fromEntry) {} @Override public String getName() { return ""; } @Override public int getOffset() { return offset; } @Override public StructEntry getParent() { return null; } @Override public int getSize() { return 0; } @Override public ByteBuffer getDataBuffer() { return StreamUtils.getByteBuffer(0); } @Override public List<StructEntry> getStructChain() { return null; } @Override public void setOffset(int newoffset) { this.offset = newoffset; } @Override public void setParent(StructEntry parent) {} } }