GordianCoreZipReadFile.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.zip;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipFileContents;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipFileEntry;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipLock;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipReadFile;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianDataConverter;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianIOException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keyset.GordianCoreKeySet;
import io.github.tonywasher.joceanus.gordianknot.impl.core.stream.GordianStreamManager;
import io.github.tonywasher.joceanus.gordianknot.impl.core.zip.GordianCoreZipLock.GordianUnlockNotify;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Class used to extract from a ZipFile.
 */
public class GordianCoreZipReadFile
        implements GordianZipReadFile, GordianUnlockNotify {
    /**
     * The extension size for the buffer.
     */
    private static final int BUFFERSIZE = 1024;

    /**
     * Lock for this zip file.
     */
    private final GordianCoreZipLock theLock;

    /**
     * The contents of this zip file.
     */
    private GordianCoreZipFileContents theContents;

    /**
     * The zip file contents.
     */
    private final byte[] theZipFile;

    /**
     * KeySet for this zip file.
     */
    private GordianCoreKeySet theKeySet;

    /**
     * The header bytes.
     */
    private final byte[] theHeader;

    /**
     * Constructor.
     *
     * @param pFactory     the factory
     * @param pInputStream the input stream to read
     * @throws GordianException on error
     */
    GordianCoreZipReadFile(final GordianFactory pFactory,
                           final InputStream pInputStream) throws GordianException {
        /* Protect against exceptions */
        try (BufferedInputStream myInBuffer = new BufferedInputStream(pInputStream);
             ByteArrayOutputStream myOutBuffer = new ByteArrayOutputStream()) {
            /* Read the Zip file into memory */
            myInBuffer.transferTo(myOutBuffer);
            theZipFile = myOutBuffer.toByteArray();

            /* Handle exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Exception accessing Zip file", e);
        }

        /* Protect against exceptions */
        try (ByteArrayInputStream myInBuffer = new ByteArrayInputStream(theZipFile);
             ZipInputStream myHdrStream = new ZipInputStream(myInBuffer)) {
            /* Create the file contents */
            theContents = new GordianCoreZipFileContents();

            /* Loop through the Zip file entries */
            ZipEntry myEntry;
            while (true) {
                /* Read next entry */
                myEntry = myHdrStream.getNextEntry();

                /* If this is EOF or a header record break the loop */
                if (myEntry == null
                        || myEntry.getExtra() != null) {
                    break;
                }

                /* Add to list of contents */
                theContents.addZipFileEntry(myEntry);
            }

            /* If we have a header */
            if (myEntry != null) {
                /* Pick up security lock */
                theLock = new GordianCoreZipLock(pFactory, this, myEntry.getExtra());
                theHeader = readHeader(myHdrStream);
            } else {
                /* Record no security */
                theLock = null;
                theHeader = null;
            }

            /* Catch exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Exception accessing Zip file", e);
        }
    }

    @Override
    public boolean isEncrypted() {
        return theLock != null;
    }

    @Override
    public GordianZipFileContents getContents() {
        return theContents;
    }

    @Override
    public GordianZipLock getLock() {
        return theLock;
    }

    @Override
    public void notifyUnlock() throws GordianException {
        /* Access the keySet */
        final GordianCoreKeySet myKeySet = (GordianCoreKeySet) theLock.getKeySet();

        /* Parse the decrypted header */
        final byte[] myBytes = myKeySet.decryptBytes(theHeader);
        theContents = new GordianCoreZipFileContents(GordianDataConverter.byteArrayToString(myBytes));

        /* Access the security details */
        final GordianCoreZipFileEntry myHeader = theContents.getHeader();

        /* Reject if the entry is not found */
        if (myHeader == null) {
            throw new GordianDataException("Header record not found.");
        }

        /* Obtain encoded keySet */
        final byte[] mySecuredKeySet = myHeader.getHash();
        theKeySet = myKeySet.deriveKeySet(mySecuredKeySet);
    }

    /**
     * Read the header.
     *
     * @param pHdrStream the header stream
     * @return the header
     * @throws IOException on error
     */
    private static byte[] readHeader(final InputStream pHdrStream) throws IOException {
        /* Initialise variables */
        int myLen = 0;
        int mySpace = BUFFERSIZE;
        byte[] myBuffer = new byte[BUFFERSIZE];

        /* Loop */
        while (true) {
            /* Read the header entry */
            final int myRead = pHdrStream.read(myBuffer, myLen, mySpace);
            if (myRead == -1) {
                break;
            }

            /* Adjust buffer */
            myLen += myRead;
            mySpace -= myRead;

            /* If we have finished up the buffer */
            if (mySpace == 0) {
                /* Increase the buffer */
                myBuffer = Arrays.copyOf(myBuffer, myLen
                        + BUFFERSIZE);
                mySpace += BUFFERSIZE;
            }
        }

        /* Cut down the buffer to size */
        return Arrays.copyOf(myBuffer, myLen);
    }

    @Override
    public Document readXMLDocument(final GordianZipFileEntry pFile) throws GordianException {
        /* Access the entry as an input stream */
        try (InputStream myInputStream = createInputStream(pFile)) {
            /* Create a Document builder */
            final DocumentBuilderFactory myFactory = DocumentBuilderFactory.newInstance();
            myFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            myFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
            final DocumentBuilder myBuilder = myFactory.newDocumentBuilder();

            /* Build the document from the input stream */
            return myBuilder.parse(myInputStream);

            /* Catch exceptions */
        } catch (IOException
                 | ParserConfigurationException
                 | SAXException e) {
            throw new GordianIOException("Failed to parse Document", e);
        }
    }

    @Override
    public InputStream createInputStream(final GordianZipFileEntry pFile) throws GordianException {
        /* Check that entry belongs to this zip file */
        if (!((GordianCoreZipFileEntry) pFile).getParent().equals(theContents)) {
            throw new GordianDataException("File does not belong to Zip file");
        }

        /* Declare control variables */
        ZipInputStream myZipFile = null;
        InputStream myResult = null;

        /* Protect against exceptions */
        final GordianCoreZipFileEntry myFile = (GordianCoreZipFileEntry) pFile;
        try {
            /* Open the zip file for reading */
            final ByteArrayInputStream myInBuffer = new ByteArrayInputStream(theZipFile);
            myZipFile = new ZipInputStream(myInBuffer);

            /* Access the name of the file entry */
            final String myName = myFile.getZipName();
            ZipEntry myEntry;

            /* Loop through the Zip file entries */
            do {
                /* Read the entry */
                myEntry = myZipFile.getNextEntry();

                /* Break if we reached EOF or found the correct entry */
            } while (myEntry != null
                    && myEntry.getName().compareTo(myName) != 0);

            /* Handle entry not found */
            if (myEntry == null) {
                myZipFile.close();
                throw new GordianDataException("File not found - "
                        + pFile.getFileName());
            }

            /* If the file is encrypted */
            if (isEncrypted()) {
                /* Create a StreamManager */
                final GordianStreamManager myManager = new GordianStreamManager(theKeySet);

                /* Build input stream */
                myResult = myManager.buildInputStream(myFile.buildInputList(), myZipFile);

                /* Else we are already OK */
            } else {
                myResult = myZipFile;
            }

            /* return the new stream */
            return myResult;

            /* Catch exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Exception creating new Input stream", e);

        } finally {
            /* Close the ZipFile on error */
            if (myZipFile != null && myResult == null) {
                GordianStreamManager.cleanUpInputStream(myZipFile);
            }
        }
    }
}