/*
* Copyright 2011 Warren Falk
* This program is distributed under the terms of the GNU General Public License (LGPL)
*
* This file is part of lzma-java
*
* lzma-java is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* See https://bitbucket.org/warren/lzma-java for the latest version
*/
package warrenfalk.lzma;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import warrenfalk.util.BinCoder;
import SevenZip.Compression.LZMA.Decoder;
/**
* This particular implementation exposes an input stream which, when read from, gets
* its data from an output stream from an Lzma SDK decoder on another thread.
*
* The drawback of this is that exceptions on the lzma decoding (input stream) side of
* the operation are not thrown in the thread that's supplying them with data as is usually
* the case, so these will be marshaled to the calling thread and rethrown at the next
* available opportunity (i.e. next client call).
*
* @author warren
*
*/
public class LzmaInputStream extends InputStream implements
ReadableByteChannel {
public final Marshaler marshaler;
public final DecodeThread thread;
/**
* Creates an LzmaInputStream with default settings
* @param stream
* @throws IOException
*/
public LzmaInputStream(InputStream stream) throws IOException {
this(stream, 4096);
}
public LzmaInputStream(InputStream stream, int marshalBufferSize) throws IOException {
this(stream, new byte[marshalBufferSize]);
}
public LzmaInputStream(InputStream stream, byte[] marshalBuffer) throws IOException {
marshaler = new Marshaler(marshalBuffer);
thread = new DecodeThread(stream);
thread.start();
}
public class DecodeThread extends Thread {
final Decoder decoder;
final InputStream stream;
final long outSize;
public DecodeThread(InputStream stream) throws IOException {
this.decoder = new Decoder();
byte[] properties = new byte[5];
if (stream.read(properties, 0, properties.length) != properties.length)
throw new IOException("unexpected end of stream");
this.decoder.SetDecoderProperties(properties);
long outSize = BinCoder.getInt8(stream);
this.outSize = outSize;
this.stream = stream;
}
@Override
public void run() {
try {
decoder.Code(stream, marshaler, outSize);
marshaler.end();
}
catch (Throwable t) {
marshaler._throw(t);
}
}
}
public class Marshaler extends OutputStream {
final byte[] buffer;
int wcursor;
int rcursor;
int size;
boolean eos;
Throwable t;
public Marshaler(byte[] marshalBuffer) {
this.buffer = marshalBuffer;
}
public int read(byte[] b, int off, int len) throws IOException {
try {
synchronized (buffer) {
while (size == 0) {
if (eos)
return -1;
buffer.wait();
}
boolean wasFull = buffer.length == size;
if (len > size)
len = size;
// a copy operation can be split into two parts if the data to be read is wrapped to the beginning of the buffer
int len1 = len;
int end = rcursor + len;
int len2 = end - buffer.length;
if (len2 > 0) {
len1 -= len2;
System.arraycopy(buffer, 0, b, off + len1, len2);
}
if (len1 > 0) {
System.arraycopy(buffer, rcursor, b, off, len1);
}
rcursor = (rcursor + len) % buffer.length;
size -= len;
if (wasFull)
buffer.notify();
rethrow();
}
return len;
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
public int read(ByteBuffer b) throws IOException {
try {
int len;
synchronized (buffer) {
while (size == 0) {
if (eos)
return -1;
buffer.wait();
}
boolean wasFull = buffer.length == size;
len = b.remaining();
if (len > size)
len = size;
// a copy operation can be split into two parts if the data to be read is wrapped to the beginning of the buffer
int len1 = len;
int end = rcursor + len;
int len2 = end - buffer.length;
if (len2 > 0)
len1 -= len2;
if (len1 > 0)
b.put(buffer, rcursor, len1);
if (len2 > 0)
b.put(buffer, 0, len2);
rcursor = (rcursor + len) % buffer.length;
size -= len;
if (wasFull)
buffer.notify();
rethrow();
}
return len;
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
public int read() throws IOException {
try {
int b;
synchronized (buffer) {
while (size == 0) {
if (eos)
return -1;
buffer.wait();
}
boolean wasFull = buffer.length == size;
// a copy operation can be split into two parts if the data to be read is wrapped to the beginning of the buffer
b = buffer[rcursor++];
rcursor %= buffer.length;
size--;
if (wasFull)
buffer.notify();
rethrow();
}
return b;
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
@Override
public void write(byte[] b, int off, final int len) throws IOException {
try {
int remain = len;
while (remain > 0) {
synchronized (buffer) {
if (eos)
throw new IOException("Cannot write to a stream after it has been closed");
while (buffer.length == size)
buffer.wait();
boolean wasEmpty = 0 == size;
int avail = buffer.length - size;
int write = remain;
if (write > avail)
write = avail;
if (write > 0) {
// a copy operation can be split into two parts if the data to be written is wrapped to the beginning of the buffer
int len1 = write;
int end = wcursor + write;
int len2 = end - buffer.length;
if (len2 > 0) {
len1 -= len2;
System.arraycopy(b, off + len1, buffer, 0, len2);
}
if (len1 > 0) {
System.arraycopy(b, off, buffer, wcursor, len1);
}
size += write;
wcursor = (wcursor + write) % buffer.length;
if (wasEmpty)
buffer.notify();
remain -= write;
off += write;
}
}
}
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
@Override
public void write(int b) throws IOException {
try {
synchronized (buffer) {
if (eos)
throw new IOException("Cannot write to a stream after it has been closed");
while (buffer.length == size)
buffer.wait();
boolean wasEmpty = 0 == size;
buffer[wcursor++] = (byte)b;
if (wcursor == buffer.length)
wcursor = 0;
size++;
if (wasEmpty)
buffer.notify();
}
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
public void end() throws IOException {
synchronized (buffer) {
eos = true;
buffer.notify();
}
}
void _throw(Throwable t) {
this.t = t;
}
public void rethrow() throws IOException {
if (this.t == null)
return;
if (this.t instanceof IOException)
throw (IOException)this.t;
if (this.t instanceof RuntimeException)
throw (RuntimeException)this.t;
if (this.t instanceof Error)
throw (Error)this.t;
throw new IOException(t);
}
}
@Override
public void close() throws IOException {
marshaler.end();
try {
thread.join();
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
marshaler.rethrow();
super.close();
}
@Override
public boolean isOpen() {
// TODO: need to think about whether this is correct or not
return true;
}
@Override
public int read(ByteBuffer b) throws IOException {
return marshaler.read(b);
}
@Override
public int read() throws IOException {
return marshaler.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return marshaler.read(b, off, len);
}
}