// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.util; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Vector; import org.infinity.util.io.StreamUtils; // Create a new IE game resource from scratch. public class ResourceStructure implements Cloneable { public static final int ID_BYTE = 0x01; // integer, fixed size=1 public static final int ID_WORD = 0x02; // integer, fixed size=2 public static final int ID_DWORD = 0x04; // integer, fixed size=4 public static final int ID_STRREF = 0x04; // integer, fixed size=4 (alias for ID_DWORD) public static final int ID_RESREF = 0x08; // string, default size=8 public static final int ID_STRING = 0x10; // string, variable size public static final int ID_BUFFER = 0x11; // ByteBuffer, variable size private Vector<Item> list; private int cursize; public ResourceStructure() { super(); list = new Vector<Item>(); cursize = 0; } // Specialized method: for fixed-sized entries only, value=0 public int add(int type) { return insert(cursize, type); } // Specialized method: value argument interpreted as size for ID_STRING and ID_ARRAY public int add(int type, int value) { return insert(cursize, type, value); } // Specialized method: for strings only, string length determines size for ID_STRING public int add(int type, String value) { return insert(cursize, type, value); } public int add(int type, int size, Object value) { return insert(cursize, type, size, value); } // Specialized method: for fixed-sized entries only, value=0 public int insert(int offset, int type) { switch (type) { case ID_BYTE: return insert(offset, type, type & 0xf, new Byte((byte)0)); case ID_WORD: return insert(offset, type, type & 0xf, new Short((short)0)); case ID_DWORD: return insert(offset, type, type & 0xf, new Integer(0)); case ID_RESREF: return insert(offset, type, type & 0xf, ""); default: throw new IllegalArgumentException(); } } // Specialized method: value argument interpreted as size for ID_STRING and ID_ARRAY public int insert(int offset, int type, int value) { switch (type) { case ID_BYTE: return insert(offset, type, type & 0xf, new Byte((byte)value)); case ID_WORD: return insert(offset, type, type & 0xf, new Short((short)value)); case ID_DWORD: return insert(offset, type, type & 0xf, new Integer(value)); case ID_STRING: return insert(offset, type, value, null); case ID_BUFFER: return insert(offset, type, value, null); default: throw new IllegalArgumentException(); } } // Specialized method: for strings only, string length determines size for ID_STRING public int insert(int offset, int type, String value) { if (type == ID_RESREF) { return insert(offset, type, type & 0xf, value); } else if (type == ID_STRING && value != null) { ByteBuffer buffer = StreamUtils.getByteBuffer(value.getBytes()); return insert(offset, ID_BUFFER, buffer.limit(), buffer); } else { throw new IllegalArgumentException(); } } // Catch-all method: returns size of the whole structure after insertion public int insert(int offset, int type, int size, Object value) { if (type == ID_BYTE || type == ID_WORD || type == ID_DWORD) { size = type & 0xf; } return insertItem(offset, type, size, value); } // Remove item at offset, returns new size of the structure public int remove(int offset) { return removeItem(offset); } public void clear() { list.clear(); cursize = 0; } public int size() { return cursize; } public boolean isEmpty() { return list.isEmpty(); } public Item get(int offset) { return getItem(offset); } // returns whole structure as sequence of bytes public ByteBuffer getBuffer() { if (list.size() == 0) { return StreamUtils.getByteBuffer(0); } ByteBuffer buf = StreamUtils.getByteBuffer(cursize); for (final Item e : list) { buf.put(e.toBuffer()); } buf.position(0); return buf; } // returns item at specified offset as sequence of bytes public ByteBuffer getBuffer(int offset) { return getItem(offset).toBuffer(); } public void write(OutputStream os) throws IOException { StreamUtils.writeBytes(os, getBuffer()); } private boolean isValidItem(int type, int size, Object value) { if (size <= 0) return false; switch (type) { case ID_BYTE: case ID_WORD: case ID_DWORD: return (size == (type & 0xf)); case ID_RESREF: case ID_STRING: return (value == null || (value instanceof String && ((String)value).getBytes().length <= size)); case ID_BUFFER: return ((value == null) || (value instanceof byte[]) || (value instanceof ByteBuffer && ((ByteBuffer)value).limit() <= size)); default: return false; } } // returns the index of the item located at offset private int indexOf(int offset) { if (offset < 0 || offset >= cursize) { throw new IndexOutOfBoundsException(); } int curofs = 0; for (int i = 0; i < list.size(); i++) { int size = list.get(i).getSize(); if (offset >= curofs && offset < curofs + size) { return i; } curofs += size; } throw new IndexOutOfBoundsException(); } private int insertItem(int offset, int type, int size, Object value) { if (offset >= 0 && offset <= cursize) { if (!isValidItem(type, size, value)) { throw new IllegalArgumentException(); } if (offset == cursize) { list.add(new Item(type, size, value)); } else { list.insertElementAt(new Item(type, size, value), indexOf(offset)); } cursize += size; return cursize; } else throw new IndexOutOfBoundsException(); } private int removeItem(int offset) { if (offset >= 0 && offset < cursize) { int index = indexOf(offset); int size = list.get(index).getSize(); list.remove(index); cursize -= size; } else throw new IndexOutOfBoundsException(); return cursize; } private Item getItem(int offset) { return list.get(indexOf(offset)); } // --------------------- Begin Interface Cloneable --------------------- @Override public Object clone() { ResourceStructure o = new ResourceStructure(); o.list = new Vector<Item>(list); o.cursize = cursize; return o; } // --------------------- End Interface Cloneable --------------------- @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof ResourceStructure)) return false; return (list.equals(((ResourceStructure)obj).list) && cursize == ((ResourceStructure)obj).cursize); } @Override public int hashCode() { return super.hashCode() + list.hashCode() + cursize; } // -------------------------- INNER CLASSES -------------------------- public final class Item implements Cloneable { private final int type; private final int size; private final Object value; public int getType() { return type; } // size always in bytes public int getSize() { return size; } public Object getValue() { return value; } // --------------------- Begin Interface Cloneable --------------------- @Override public Object clone() { return new Item(type, size, value); } // --------------------- End Interface Cloneable --------------------- @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof Item)) return false; return (type == ((Item)obj).type && size == ((Item)obj).size && value == ((Item)obj).value); } @Override public int hashCode() { return super.hashCode() + type + size + value.hashCode(); } private Item(int type, int size, Object value) { this.type = type; this.size = (type == ID_BYTE || type == ID_WORD || type == ID_DWORD) ? type & 0xf : size; if (type == ID_RESREF || type == ID_STRING) { this.value = (value == null || !(value instanceof String)) ? "" : value; } else if (type == ID_BUFFER) { if (value instanceof byte[]) { this.value = StreamUtils.getByteBuffer((byte[])value); } else if (value instanceof ByteBuffer) { this.value = (ByteBuffer)value; } else { this.value = StreamUtils.getByteBuffer(size); } } else { this.value = value; } } // returns the current item as a ByteBuffer object private ByteBuffer toBuffer() { if (size <= 0) throw new IndexOutOfBoundsException(); ByteBuffer buf = StreamUtils.getByteBuffer(size); switch (type) { case ID_BYTE: if (value != null) buf.put((Byte)value); break; case ID_WORD: if (value != null) buf.putShort((Short)value); break; case ID_DWORD: if (value != null) buf.putInt((Integer)value); break; case ID_RESREF: case ID_STRING: if (value instanceof String) { byte[] b = ((String)value).getBytes(); buf.put(Arrays.copyOf(b, b.length <= size ? b.length : size)); } break; case ID_BUFFER: if (value instanceof ByteBuffer) { ByteBuffer b = (ByteBuffer)value; b.position(0); int len = Math.min(b.limit(), size); int limit = b.limit(); if (len < b.limit()) { b.limit(len); } buf.put(b); b.limit(limit); b.position(0); } break; } buf.position(0); return buf; } } }