GordianZipFileProperties.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.impl.core.base.GordianDataConverter;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * Class represents the properties of an encrypted file in the Zip file.
 */
public class GordianZipFileProperties {
    /**
     * The property separator.
     */
    private static final char SEP_PROPERTY = '/';

    /**
     * The value separator.
     */
    static final char SEP_VALUE = '=';

    /**
     * The long separator.
     */
    private static final char SEP_LONG = '!';

    /**
     * The Buffer length.
     */
    private static final int BUFFER_LEN = 1000;

    /**
     * The Value buffer length.
     */
    private static final char BUFFER_VALLEN = 200;

    /**
     * List of properties.
     */
    private final List<Property> theList;

    /**
     * Constructor.
     */
    protected GordianZipFileProperties() {
        /* Allocate the array */
        theList = new ArrayList<>();
    }

    /**
     * Constructor from encoded string.
     *
     * @param pCodedString the encoded string
     * @throws GordianException on error
     */
    protected GordianZipFileProperties(final String pCodedString) throws GordianException {
        /* Initialise normally */
        this();

        /* Wrap string in a string builder */
        final StringBuilder myString = new StringBuilder(pCodedString);
        final String myPropSep = Character.toString(SEP_PROPERTY);

        /* while we have separators in the string */
        while (true) {
            /* Locate next separator and break if not found */
            final int myLoc = myString.indexOf(myPropSep);
            if (myLoc == -1) {
                break;
            }

            /* Parse the encoded property and remove it from the buffer */
            parseEncodedProperty(myString.substring(0, myLoc));
            myString.delete(0, myLoc + 1);
        }

        /* Parse the remaining property */
        parseEncodedProperty(myString.toString());
    }

    /**
     * Set the named property.
     *
     * @param pName  the name of the property
     * @param pValue the Value of the property
     */
    protected void setProperty(final String pName,
                               final String pValue) {
        /* Set the new value */
        setProperty(pName, GordianDataConverter.stringToByteArray(pValue));
    }

    /**
     * Set the named property.
     *
     * @param pName  the name of the property
     * @param pValue the Value of the property
     */
    protected void setProperty(final String pName,
                               final byte[] pValue) {
        /* Determine whether we are setting a null value */
        final boolean isNull = pValue == null;

        /* Access any existing property */
        Property myProperty = getProperty(pName);

        /* If the property does not exist */
        if (myProperty == null) {
            /* If we have a value */
            if (!isNull) {
                /* Create the new property */
                myProperty = new Property(theList, pName);

                /* Set the new value */
                myProperty.setByteValue(pValue);
            }

            /* else if the property now has no value */
        } else if (isNull
                && myProperty.getLongValue() == null) {
            /* Remove the value from the list */
            theList.remove(myProperty);

            /* else just set the value */
        } else {
            /* Set the new value */
            myProperty.setByteValue(pValue);
        }
    }

    /**
     * Set the named property.
     *
     * @param pName  the name of the property
     * @param pValue the Value of the property
     */
    protected void setProperty(final String pName,
                               final Long pValue) {
        /* Determine whether we are setting a null value */
        final boolean isNull = pValue == null;

        /* Access any existing property */
        Property myProperty = getProperty(pName);

        /* If the property does not exist */
        if (myProperty == null) {
            /* If we have a value */
            if (!isNull) {
                /* Create the new property */
                myProperty = new Property(theList, pName);

                /* Set the new value */
                myProperty.setLongValue(pValue);
            }

            /* else if the property now has no value */
        } else if (isNull
                && myProperty.getByteValue() == null) {
            /* Remove the value from the list */
            theList.remove(myProperty);

            /* else just set the value */
        } else {
            /* Set the new value */
            myProperty.setLongValue(pValue);
        }
    }

    /**
     * Obtain the string value of the named property.
     *
     * @param pName the name of the property
     * @return the value of the property or <code>null</code> if the property does not exist
     */
    protected String getStringProperty(final String pName) {
        /* Access the property */
        final byte[] myValue = getByteProperty(pName);

        /* Return the value */
        return (myValue == null)
                ? null
                : GordianDataConverter.byteArrayToString(myValue);
    }

    /**
     * Obtain the bytes value of the named property.
     *
     * @param pName the name of the property
     * @return the value of the property or <code>null</code> if the property does not exist
     */
    protected byte[] getByteProperty(final String pName) {
        /* Access the property */
        final Property myProperty = getProperty(pName);

        /* Return the value */
        return (myProperty == null)
                ? null
                : myProperty.getByteValue();
    }

    /**
     * Obtain the long value of the named property.
     *
     * @param pName the name of the property
     * @return the value of the property or <code>-1</code> if the property does not exist
     */
    protected Long getLongProperty(final String pName) {
        /* Access the property */
        final Property myProperty = getProperty(pName);

        /* Return the value */
        return (myProperty == null)
                ? null
                : myProperty.getLongValue();
    }

    /**
     * Obtain the named property from the list.
     *
     * @param pName the name of the property
     * @return the value of the property or <code>null</code> if the property does not exist
     */
    private Property getProperty(final String pName) {
        /* Loop through the properties */
        final Iterator<Property> myIterator = theList.iterator();
        while (myIterator.hasNext()) {
            /* Access next property */
            final Property myProperty = myIterator.next();

            /* Check the property name */
            final int iDiff = pName.compareTo(myProperty.getName());

            /* If this is the required property, return it */
            if (iDiff == 0) {
                return myProperty;
            }

            /* If this property is later than the required name, no such property */
            if (iDiff < 0) {
                break;
            }
        }

        /* Return not found */
        return null;
    }

    /**
     * Encode the properties.
     *
     * @return the encoded string
     */
    protected String encodeProperties() {
        final StringBuilder myString = new StringBuilder(BUFFER_LEN);
        final StringBuilder myValue = new StringBuilder(BUFFER_VALLEN);

        /* Loop through the properties */
        final Iterator<Property> myIterator = theList.iterator();
        while (myIterator.hasNext()) {
            /* Access next property */
            final Property myProperty = myIterator.next();

            /* Build the value string */
            myValue.setLength(0);
            myValue.append(myProperty.getName());
            myValue.append(SEP_VALUE);

            /* If we have a byte value */
            if (myProperty.getByteValue() != null) {
                /* Add the byte value as a Hex String */
                myValue.append(GordianDataConverter.bytesToHexString(myProperty.getByteValue()));
            }

            /* Add the value separator */
            myValue.append(SEP_LONG);

            /* If we have a long value */
            if (myProperty.getLongValue() != null) {
                /* Add the long value as a Hex String */
                myValue.append(GordianDataConverter.longToHexString(myProperty.getLongValue()));
            }

            /* Add the value to the string */
            if (myString.length() > 0) {
                myString.append(SEP_PROPERTY);
            }
            myString.append(myValue);
        }

        /* Return the encoded string */
        return myString.toString();
    }

    /**
     * Parse the encoded string representation to obtain the property.
     *
     * @param pValue the encoded property
     * @throws GordianException on error
     */
    private void parseEncodedProperty(final String pValue) throws GordianException {
        /* Locate the Value separator in the string */
        int myLoc = pValue.indexOf(SEP_VALUE);

        /* Check that we found the value separator */
        if (myLoc == -1) {
            throw new GordianDataException("Missing value separator: "
                    + pValue);
        }

        /* Split the values and name */
        final String myName = pValue.substring(0, myLoc);

        /* If the name is already present reject it */
        if (getProperty(myName) != null) {
            throw new GordianDataException("Duplicate name: "
                    + pValue);
        }

        /* Locate the Long separator in the string */
        String myBytes = pValue.substring(myLoc + 1);
        myLoc = myBytes.indexOf(SEP_LONG);

        /* Check that we found the long separator */
        if (myLoc == -1) {
            throw new GordianDataException("Missing long separator: "
                    + pValue);
        }

        /* Access the separate byte and long values */
        final int myLen = myBytes.length();
        final String myLong = myLoc < myLen - 1
                ? myBytes.substring(myLoc + 1)
                : null;
        myBytes = myLoc > 0
                ? myBytes.substring(0, myLoc)
                : null;

        /* Must have at least one of Bytes/Long */
        if (myBytes == null
                && myLong == null) {
            throw new GordianDataException("Invalid property: "
                    + myName);
        }

        /* Create a new property */
        final Property myProperty = new Property(theList, myName);

        /* If we have a bytes array */
        if (myBytes != null) {
            /* Set the bytes value */
            myProperty.setByteValue(GordianDataConverter.hexStringToBytes(myBytes));
        }

        /* If we have a long value */
        if (myLong != null) {
            /* Access the bytes value */
            myProperty.setLongValue(GordianDataConverter.hexStringToLong(myLong));
        }
    }

    /**
     * Individual Property.
     */
    private static final class Property {
        /**
         * Name of property.
         */
        private final String theName;

        /**
         * Value of property.
         */
        private byte[] theByteValue;

        /**
         * Value of property.
         */
        private Long theLongValue;

        /**
         * Standard Constructor.
         *
         * @param pList property list
         * @param pName the name of the property
         */
        Property(final List<Property> pList,
                 final String pName) {
            /* Check for invalid name */
            if (pName.indexOf(SEP_VALUE) != -1) {
                throw new IllegalArgumentException("Invalid property name - "
                        + pName);
            }

            /* Store name */
            theName = pName;

            /* Loop through the properties in the list */
            int iIndex = 0;
            final Iterator<Property> myIterator = pList.iterator();
            while (myIterator.hasNext()) {
                /* Access next property */
                final Property myProperty = myIterator.next();

                /* Check the property name */
                final int iDiff = pName.compareTo(myProperty.getName());

                /* If this property is later than us */
                if (iDiff < 0) {
                    break;
                }

                /* Reject attempt to add duplicate name */
                if (iDiff == 0) {
                    throw new IllegalArgumentException("Duplicate property - "
                            + pName);
                }

                /* Increment index */
                iIndex++;
            }

            /* Add into the list at the correct point */
            pList.add(iIndex, this);
        }

        /**
         * Obtain the name of the property.
         *
         * @return the name of the property
         */
        String getName() {
            return theName;
        }

        /**
         * Obtain the byte value of the property.
         *
         * @return the value of the property
         */
        byte[] getByteValue() {
            return theByteValue;
        }

        /**
         * Obtain the long value of the property.
         *
         * @return the value of the property
         */
        Long getLongValue() {
            return theLongValue;
        }

        /**
         * Set the byte value.
         *
         * @param pValue the new value
         */
        void setByteValue(final byte[] pValue) {
            theByteValue = (pValue == null)
                    ? null
                    : Arrays.copyOf(pValue, pValue.length);
        }

        /**
         * Set the long value.
         *
         * @param pValue the new value
         */
        void setLongValue(final Long pValue) {
            theLongValue = pValue;
        }
    }
}