package org.apache.poi.poifs.filesystem;
import static org.apache.poi.util.LittleEndianConsts.INT_SIZE;
import static org.apache.poi.util.LittleEndianConsts.LONG_SIZE;
import static org.apache.poi.util.LittleEndianConsts.SHORT_SIZE;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Iterator;
import org.apache.poi.poifs.property.DocumentProperty;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianInput;
public final class DocumentInputStream extends InputStream implements LittleEndianInput {
private static final int EOF = -1;
private int _current_offset;
private int _current_block_count;
private int _marked_offset;
private int _marked_offset_count;
private final int _document_size;
private boolean _closed;
private final POIFSDocument _document;
private Iterator<ByteBuffer> _data;
private ByteBuffer _buffer;
public DocumentInputStream(DocumentEntry document) throws IOException {
if (!(document instanceof DocumentNode)) {
throw new IOException("Cannot open internal document storage, " + document + " not a Document Node");
}
_current_offset = 0;
_current_block_count = 0;
_marked_offset = 0;
_marked_offset_count = 0;
_document_size = document.getSize();
_closed = false;
DocumentNode doc = (DocumentNode)document;
DocumentProperty property = (DocumentProperty)doc.getProperty();
_document = new POIFSDocument(
property,
((DirectoryNode)doc.getParent()).getFileSystem()
);
_data = _document.getBlockIterator();
}
public DocumentInputStream(POIFSDocument document) {
_current_offset = 0;
_current_block_count = 0;
_marked_offset = 0;
_marked_offset_count = 0;
_document_size = document.getSize();
_closed = false;
_document = document;
_data = _document.getBlockIterator();
}
@Override
public int available() {
return remainingBytes();
}
private int remainingBytes() {
if (_closed) {
throw new IllegalStateException("cannot perform requested operation on a closed stream");
}
return _document_size - _current_offset;
}
@Override
public void close() {
_closed = true;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public void mark(int ignoredReadlimit) {
_marked_offset = _current_offset;
_marked_offset_count = Math.max(0, _current_block_count - 1);
}
@Override
public int read() throws IOException {
dieIfClosed();
if (atEOD()) {
return EOF;
}
byte[] b = new byte[1];
int result = read(b, 0, 1);
if(result >= 0) {
if(b[0] < 0) {
return b[0]+256;
}
return b[0];
}
return result;
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
dieIfClosed();
if (b == null) {
throw new IllegalArgumentException("buffer must not be null");
}
if (off < 0 || len < 0 || b.length < off + len) {
throw new IndexOutOfBoundsException("can't read past buffer boundaries");
}
if (len == 0) {
return 0;
}
if (atEOD()) {
return EOF;
}
int limit = Math.min(remainingBytes(), len);
readFully(b, off, limit);
return limit;
}
@Override
public void reset() {
if(_marked_offset == 0 && _marked_offset_count == 0) {
_current_block_count = _marked_offset_count;
_current_offset = _marked_offset;
_data = _document.getBlockIterator();
_buffer = null;
return;
}
_data = _document.getBlockIterator();
_current_offset = 0;
for(int i=0; i<_marked_offset_count; i++) {
_buffer = _data.next();
_current_offset += _buffer.remaining();
}
_current_block_count = _marked_offset_count;
if(_current_offset != _marked_offset) {
_buffer = _data.next();
_current_block_count++;
int skipBy = _marked_offset - _current_offset;
_buffer.position(_buffer.position() + skipBy);
}
_current_offset = _marked_offset;
}
@Override
public long skip(long n) throws IOException {
dieIfClosed();
if (n < 0) {
return 0;
}
long new_offset = _current_offset + n;
if (new_offset < _current_offset) {
new_offset = _document_size;
} else if (new_offset > _document_size) {
new_offset = _document_size;
}
long rval = new_offset - _current_offset;
byte[] skip = IOUtils.safelyAllocate(rval, Integer.MAX_VALUE);
readFully(skip);
return rval;
}
private void dieIfClosed() throws IOException {
if (_closed) {
throw new IOException("cannot perform requested operation on a closed stream");
}
}
private boolean atEOD() {
return _current_offset == _document_size;
}
private void checkAvaliable(int requestedSize) {
if (_closed) {
throw new IllegalStateException("cannot perform requested operation on a closed stream");
}
if (requestedSize > _document_size - _current_offset) {
throw new RuntimeException("Buffer underrun - requested " + requestedSize
+ " bytes but " + (_document_size - _current_offset) + " was available");
}
}
@Override
public void readFully(byte[] buf) {
readFully(buf, 0, buf.length);
}
@Override
public void readFully(byte[] buf, int off, int len) {
if (len < 0) {
throw new RuntimeException("Can't read negative number of bytes");
}
checkAvaliable(len);
int read = 0;
while(read < len) {
if(_buffer == null || _buffer.remaining() == 0) {
_current_block_count++;
_buffer = _data.next();
}
int limit = Math.min(len-read, _buffer.remaining());
_buffer.get(buf, off+read, limit);
_current_offset += limit;
read += limit;
}
}
@Override
public void readPlain(byte[] buf, int off, int len) {
readFully(buf, off, len);
}
@Override
public byte readByte() {
return (byte) readUByte();
}
@Override
public double readDouble() {
return Double.longBitsToDouble(readLong());
}
@Override
public long readLong() {
checkAvaliable(LONG_SIZE);
byte[] data = new byte[LONG_SIZE];
readFully(data, 0, LONG_SIZE);
return LittleEndian.getLong(data, 0);
}
@Override
public short readShort() {
checkAvaliable(SHORT_SIZE);
byte[] data = new byte[SHORT_SIZE];
readFully(data, 0, SHORT_SIZE);
return LittleEndian.getShort(data);
}
@Override
public int readInt() {
checkAvaliable(INT_SIZE);
byte[] data = new byte[INT_SIZE];
readFully(data, 0, INT_SIZE);
return LittleEndian.getInt(data);
}
public long readUInt() {
int i = readInt();
return i & 0xFFFFFFFFL;
}
@Override
public int readUShort() {
checkAvaliable(SHORT_SIZE);
byte[] data = new byte[SHORT_SIZE];
readFully(data, 0, SHORT_SIZE);
return LittleEndian.getUShort(data);
}
@Override
public int readUByte() {
checkAvaliable(1);
byte[] data = new byte[1];
readFully(data, 0, 1);
if (data[0] >= 0)
return data[0];
return data[0] + 256;
}
}