// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.key; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.SequenceInputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.zip.Inflater; import org.infinity.NearInfinity; import org.infinity.gui.WindowBlocker; import org.infinity.util.io.ByteBufferInputStream; import org.infinity.util.io.StreamUtils; /** * Provides read operations for block-compressed BIFC V1.0 archives. */ public class BIFCReader extends AbstractBIFFReader { private final WindowBlocker blocker; private int uncSize; private int numFiles, numTilesets; protected BIFCReader(Path file) throws Exception { super(file); this.blocker = new WindowBlocker(NearInfinity.getInstance()); open(); } @Override public synchronized void open() throws Exception { try (FileChannel channel = FileChannel.open(getFile(), StandardOpenOption.READ)) { String sigver = StreamUtils.readString(channel, 8); if (!"BIFCV1.0".equals(sigver)) { throw new Exception("Invalid BIFF header"); } this.uncSize = StreamUtils.readInt(channel); if (this.uncSize < 0) { throw new Exception("Invalid BIFF archive"); } } init(); } @Override public Type getType() { return Type.BIFC; } @Override public int getFileCount() { return numFiles; } @Override public int getTilesetCount() { return numTilesets; } @Override public int getBIFFSize() { return uncSize; } @Override public ByteBuffer getResourceBuffer(int locator) throws IOException { Entry entry = getEntry(locator); if (entry == null) { throw new IOException("Resource not found"); } int size; ByteBuffer buffer; if (entry.isTile) { ByteBuffer header = getTisHeader(entry.count, entry.size); buffer = StreamUtils.getByteBuffer(entry.count*entry.size + header.limit()); StreamUtils.copyBytes(header, buffer, header.limit()); size = entry.count*entry.size; } else { buffer = StreamUtils.getByteBuffer(entry.size); size = entry.size; } if (buffer.limit() > 1000000) { blocker.setBlocked(true); } try (InputStream is = new BifcInputStream( new BufferedInputStream( Files.newInputStream(getFile(), StandardOpenOption.READ)), entry.offset, size)) { StreamUtils.readBytes(is, buffer); } finally { blocker.setBlocked(false); } buffer.position(0); return buffer; } @Override public InputStream getResourceAsStream(int locator) throws IOException { Entry entry = getEntry(locator); if (entry == null) { throw new IOException("Resource not found"); } if (entry.isTile) { ByteBuffer header = getTisHeader(entry.count, entry.size); InputStream is1 = new ByteBufferInputStream(header); @SuppressWarnings("resource") InputStream is2 = new BifcInputStream( new BufferedInputStream( Files.newInputStream(getFile(), StandardOpenOption.READ)), entry.offset, entry.count*entry.size); InputStream is = new SequenceInputStream(is1, is2); return is; } else { return new BifcInputStream( new BufferedInputStream( Files.newInputStream(getFile(), StandardOpenOption.READ)), entry.offset, entry.size); } } private void init() throws Exception { try (InputStream is = new BifcInputStream( new BufferedInputStream( Files.newInputStream(getFile(), StandardOpenOption.READ)), 0, -1)) { int curOfs = 0; String sigver = StreamUtils.readString(is, 8); if (!"BIFFV1 ".equals(sigver)) { throw new Exception("Invalid decompressed BIFF signature"); } this.numFiles = StreamUtils.readInt(is); this.numTilesets = StreamUtils.readInt(is); int entryOfs = StreamUtils.readInt(is); curOfs += 20; if (entryOfs < curOfs) { throw new Exception("Invalid decompressed BIFF header"); } int remaining = entryOfs - curOfs; while (remaining > 0) { long n = is.skip(remaining); remaining -= n; } // reading file entries for (int i = 0; i < numFiles; i++) { int locator = StreamUtils.readInt(is) & 0xfffff; int offset = StreamUtils.readInt(is); int size = StreamUtils.readInt(is); short type = StreamUtils.readShort(is); is.skip(2); // unknown data addEntry(new Entry(locator, offset, size, type)); } // reading tileset entries for (int i = 0; i < numTilesets; i++) { int locator = StreamUtils.readInt(is) & 0xfffff; int offset = StreamUtils.readInt(is); int count = StreamUtils.readInt(is); int size = StreamUtils.readInt(is); short type = StreamUtils.readShort(is); is.skip(2); // unknown data addEntry(new Entry(locator, offset, count, size, type)); } } } //-------------------------- INNER CLASSES -------------------------- private static class BifcInputStream extends InputStream { private final Inflater inflater; private InputStream input; // BIFC archive as input stream private int endOffset; // the end-of-stream offset for this InputStream in decompressed data private int position; // current absolute position in decompressed data private byte[] inBuffer; // buffer for compressed data of current block private byte[] outBuffer; // buffer for decompressed data of current block private int bufOfs; // contains relative offset in current outBuffer private int bufLen; // contains actual number of bytes of data in current outBuffer /** * Constructs an InputStream over a specific section of a BIFC archive. * @param is The BIFC archive as input stream. * @param offset Start offset in decompressed BIFF data. * @param size Size of decompressed BIFF data to map. * Specify -1 to map until the end of decompressed data. */ public BifcInputStream(InputStream is, int offset, int size) throws IOException { if (is == null) { throw new NullPointerException(); } this.input = is; String sigver = StreamUtils.readString(this.input, 8); if (!"BIFCV1.0".equals(sigver)) { throw new IOException("Unsupported source BIFF signature"); } int uncSize = StreamUtils.readInt(this.input); if (offset < 0 || offset > uncSize) { throw new IOException("Start offset is out of bounds"); } if (size < 0) { size = uncSize - offset; } if (size < 0 || offset+size > uncSize) { throw new IOException("Size is out of bounds"); } this.endOffset = offset + size; this.position = 0; this.inflater = new Inflater(); this.bufOfs = 0; this.bufLen = 0; skip(offset); } @Override public int read() throws IOException { if (available() > 0) { final byte[] b = {0}; if (getData(b, 0, 1) == 1) { return b[0] & 0xff; } } return -1; } @Override public int read(byte b[], int off, int len) throws IOException { if (available() > 0) { return getData(b, off, len); } return -1; } @Override public long skip(long n) throws IOException { return Math.max(0, skipData((int)n)); } @Override public int available() throws IOException { return endOffset - position; } @Override public void close() throws IOException { if (isOpen()) { try { input.close(); } finally { synchronized (this) { input = null; } } } } private boolean isOpen() { return (input != null); } // Writes decompressed data into "buf". Returns actual number of decompressed bytes. // Updates internal data position. Returns -1 on error or end-of-stream. private synchronized int getData(byte[] buf, int ofs, int len) throws IOException { ofs = Math.max(0, ofs); len = Math.max(0, Math.min(len, endOffset - position)); if (!isOpen() || position >= endOffset || buf == null || ofs > buf.length || ofs+len > buf.length) { return -1; } int retVal = 0; while (len > 0) { try { updateBuffer(false, -1); } catch (Exception e) { throw new IOException(e.getMessage()); } // copy data into output buffer int n = Math.min(bufLen - bufOfs, len); System.arraycopy(outBuffer, bufOfs, buf, ofs, n); retVal += n; bufOfs += n; ofs += n; len -= n; } position += retVal; return retVal; } // Skips the specified number of decompressed bytes. Returns -1 on error or end-of-stream. private synchronized int skipData(int len) throws IOException { len = Math.max(0, Math.min(len, endOffset - position)); if (!isOpen() || position >= endOffset) { return -1; } int retVal = 0; while (len > 0) { try { updateBuffer(true, len); } catch (Exception e) { throw new IOException(e); } int n = Math.min(len, bufLen - bufOfs); retVal += n; bufOfs += n; len -= n; } position += retVal; return retVal; } // Decompress next block of data. // if skipOnly == true, then the next compressed block is skipped if // (condition < 0) || (condition >= uncompressed block size) private boolean updateBuffer(boolean skipOnly, int condition) throws Exception { if (bufLen == 0 || bufOfs >= bufLen) { int uncSize = StreamUtils.readInt(input); int compSize = StreamUtils.readInt(input); if (skipOnly && (condition < 0 || condition >= uncSize)) { int remaining = compSize; while (remaining > 0) { long n = input.skip(remaining); remaining -= n; } } else { if (inBuffer == null || inBuffer.length < compSize) { inBuffer = new byte[compSize]; } if (outBuffer == null || outBuffer.length < uncSize) { outBuffer = new byte[uncSize]; } input.read(inBuffer, 0, compSize); inflater.reset(); inflater.setInput(inBuffer, 0, compSize); if (inflater.inflate(outBuffer, 0, uncSize) != uncSize) { throw new Exception("Unexpected end of decompressed data"); } } bufLen = uncSize; bufOfs = 0; return true; } return false; } } }