/*
* 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.WritableByteChannel;
import warrenfalk.util.BinCoder;
import SevenZip.Compression.LZMA.Encoder;
/**
* This particular implementation exposes an output stream which, when written to, exposes
* its data as an input stream to an Lzma SDK encoder on another thread.
*
* The drawback of this is that exceptions on the lzma encoding (output 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).
*
* You must call close() when completed writing data to ensure you receive any exception
* which occurs in the output thread. The close() call will block until the operation is
* completed (either successfully or with failure)
* @author warren
*
*/
public class LzmaOutputStream extends OutputStream implements
WritableByteChannel {
public final Marshaler marshaler;
public final EncodeThread thread;
public long remain;
/**
* Creates an LzmaOutputStream with default settings
* @param stream
* @throws IOException
*/
public LzmaOutputStream(OutputStream stream) throws IOException {
this(stream, -1, 4096);
}
public LzmaOutputStream(OutputStream stream, long origSize, int marshalBufferSize) throws IOException {
this(stream, origSize, new byte[marshalBufferSize]);
}
public LzmaOutputStream(OutputStream stream, long origSize, byte[] marshalBuffer) throws IOException {
this(stream, origSize, marshalBuffer, (1 << 22), 3, 0, 2, 0x20);
}
public LzmaOutputStream(OutputStream stream, long origSize, byte[] marshalBuffer, int dictionarySize, int literalContextBits, int literalPosStateBits, int posStateBits, int fastBytes) throws IOException {
this.remain = origSize;
marshaler = new Marshaler(marshalBuffer);
thread = new EncodeThread(stream, origSize, dictionarySize, literalContextBits, literalPosStateBits, posStateBits, fastBytes);
thread.start();
}
public class EncodeThread extends Thread {
final Encoder encoder;
final OutputStream stream;
public EncodeThread(OutputStream stream, long origSize, int dictionarySize, int literalContextBits, int literalPosStateBits, int posStateBits, int fastBytes) throws IOException {
this.encoder = new Encoder();
this.encoder.SetEndMarkerMode(origSize < 0);
this.encoder.WriteCoderProperties(stream);
BinCoder.putInt8(origSize, stream);
this.encoder.SetDictionarySize(dictionarySize);
// note: in the 9.20 SDK, setting the algorithm does nothing
//this.encoder.SetAlgorithm(algorithm);
this.encoder.SetLcLpPb(literalContextBits, literalPosStateBits, posStateBits);
this.encoder.SetNumFastBytes(fastBytes);
this.stream = stream;
}
@Override
public void run() {
try {
// TODO: allow passing in a progress updater here
// note that the inSize and outSize parameters are not ever used
// the example code sets them to -1 so that's what I'm doing.
encoder.Code(marshaler, stream, -1, -1, null);
}
catch (Throwable t) {
marshaler._throw(t);
}
}
}
public class Marshaler extends InputStream {
final byte[] buffer;
int wcursor;
int rcursor;
int size;
boolean eos;
Throwable t;
public Marshaler(byte[] marshalBuffer) {
this.buffer = marshalBuffer;
}
@Override
public int read() throws IOException {
throw new RuntimeException("Marshaler.read() is not implemented yet");
}
@Override
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();
}
return len;
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
public int write(byte[] b, int off, int len) 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;
int avail = buffer.length - size;
if (len > avail)
len = avail;
if (len > 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 = len;
int end = wcursor + len;
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 += len;
wcursor = (wcursor + len) % buffer.length;
if (wasEmpty)
buffer.notify();
}
rethrow();
}
return len;
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
public int write(ByteBuffer b) throws IOException {
try {
int len;
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;
len = b.remaining();
if (len > avail)
len = avail;
if (len > 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 = len;
int end = wcursor + len;
int len2 = end - buffer.length;
if (len2 > 0)
len1 -= len2;
if (len1 > 0) {
b.get(buffer, wcursor, len1);
}
if (len2 > 0)
b.get(buffer, 0, len2);
size += len;
wcursor = (wcursor + len) % buffer.length;
if (wasEmpty)
buffer.notify();
}
rethrow();
}
return len;
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
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();
rethrow();
}
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
}
public void end() throws IOException {
synchronized (buffer) {
eos = true;
buffer.notify();
rethrow();
}
}
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 boolean isOpen() {
return !marshaler.eos;
}
@Override
public int write(ByteBuffer b) throws IOException {
if (remain >= 0 && b.remaining() > remain)
throw new IOException("Attempt to write past end of stream");
remain -= b.remaining();
return marshaler.write(b);
}
@Override
public void write(int b) throws IOException {
if (remain == 0)
throw new IOException();
remain--;
marshaler.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (remain >= 0 && len > remain)
throw new IOException("Attempt to write past end of stream");
remain -= len;
while (len > 0) {
int written = marshaler.write(b, off, len);
len -= written;
off += written;
}
}
@Override
public void close() throws IOException {
marshaler.end();
try {
thread.join();
}
catch (InterruptedException ie) {
throw new IOException(ie);
}
marshaler.rethrow();
this.thread.stream.close();
super.close();
}
}