GordianLZMAOutputStream.java

/*
 * GordianKnot: Security Suite
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.gordianknot.impl.core.stream;

import SevenZip.Compression.LZMA.Encoder;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Provides an LZMA compression OutputStream. Due to the design of the 7-Zip libraries the
 * decompression must be performed on a separate thread. A thread is created to read data from a
 * PipedStream and to compress the data to the target output stream. This class works as the wrapper
 * to write the data to be compressed to the PipedStream.
 */
final class GordianLZMAOutputStream
        extends OutputStream {
    /**
     * The sink stream to write to the encoder thread.
     */
    private final OutputStream theSink;

    /**
     * The source stream for the encoder thread.
     */
    private final InputStream theSource;

    /**
     * The target stream for the encoder thread.
     */
    private final OutputStream theTarget;

    /**
     * The encoder service.
     */
    private final GordianEncoderService theService;

    /**
     * Constructor.
     *
     * @param pOutput the output stream to wrap
     */
    GordianLZMAOutputStream(final OutputStream pOutput) {
        /* Store the target */
        theTarget = pOutput;

        /* Create the piped stream */
        final GordianPipedStream myPipe = new GordianPipedStream();
        theSink = myPipe.getSink();
        theSource = myPipe.getSource();

        /* Create encoder service */
        theService = new GordianEncoderService();
        theService.start();
    }

    /**
     * Obtain the next stream.
     *
     * @return the stream
     */
    OutputStream getNextStream() {
        return theTarget;
    }

    @Override
    public void write(final byte[] pBytes,
                      final int pOffset,
                      final int pLength) throws IOException {
        /* Check for error */
        theService.checkForError();

        /* Write to the sink */
        theSink.write(pBytes, pOffset, pLength);
    }

    @Override
    public void write(final byte[] pBytes) throws IOException {
        /* Check for error */
        theService.checkForError();

        /* Write to the sink */
        theSink.write(pBytes);
    }

    @Override
    public void write(final int pByte) throws IOException {
        /* Check for error */
        theService.checkForError();

        /* Write to the sink */
        theSink.write(pByte);
    }

    @Override
    public void flush() throws IOException {
        /* No need to flush */
    }

    @Override
    public void close() throws IOException {
        /* Close the sink */
        theSink.close();

        /* Wait for service to terminate */
        try {
            theService.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        /* Check for error */
        theService.checkForError();
    }

    /**
     * The encoder service.
     */
    private final class GordianEncoderService
            extends Thread {
        /**
         * The encoder.
         */
        private final Encoder theEncoder;

        /**
         * The error.
         */
        private IOException theError;

        /**
         * Constructor.
         */
        GordianEncoderService() {
            /* Create the encoder */
            theEncoder = new Encoder();
            theError = null;
        }

        /**
         * Check for error.
         *
         * @throws IOException on error
         */
        void checkForError() throws IOException {
            if (theError != null) {
                throw theError;
            }
        }

        @Override
        public void run() {
            try {
                /* Set end markerMode on */
                theEncoder.SetEndMarkerMode(true);

                /* Write encoder properties */
                theEncoder.WriteCoderProperties(theTarget);

                /* Encode the source stream to the target */
                theEncoder.Code(theSource, theTarget, -1, -1, null);

                /* Close the target */
                theTarget.close();

                /* Catch and record any errors */
            } catch (Exception e) {
                theError = e instanceof IOException i ? i : new IOException(e);
            }
        }
    }
}