/*******************************************************************************
 * Copyright (c) 2006, 2019 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.osgi.internal.signedcontent;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SignatureException;

/**
 * This is a simple class that processes BER structures. This class
 * uses BER processing as outlined in X.690.
 */
public class BERProcessor {
	/**
	 * This is the buffer that contains the BER structures that are being interrogated.
	 */
	byte buffer[];
	/**
	 * The offset into <code>buffer</code> to the start of the structure being interrogated.
	 * If the offset is -1 that means that we have read the last structure.
	 */
	int offset;
	/**
	 * The last valid offset in <code>buffer</code>.
	 */
	int lastOffset;
	/**
	 * The offset into <code>buffer</code> to the start of the content of the structure
	 * being interrogated.
	 */
	int contentOffset;
	/**
	 * The length of the content of the structure being interrogated.
	 */
	int contentLength;
	/**
	 * The offset into <code>buffer</code> of the end of the structure being interrogated.
	 */
	int endOffset;
	/**
	 * The class of the tag of the current structure.
	 */
	int classOfTag;
	static final int UNIVERSAL_TAGCLASS = 0;
	static final int APPLICATION_TAGCLASS = 1;
	static final int CONTEXTSPECIFIC_TAGCLASS = 2;
	static final int PRIVATE_TAGCLASS = 3;

	static final byte BOOLTAG = 1;
	static final byte INTTAG = 2;
	static final byte OIDTAG = 6;
	static final byte SEQTAG = 16;
	static final byte SETTAG = 17;
	static final byte NULLTAG = 5;

	/**
	 * Tagnames used in toString()
	 */
	static final String tagNames[] = {"<null>", "boolean", "int", "bitstring", "octetstring", "null", "objid", "objdesc", "external", "real", "enum", "pdv", "utf8", "relobjid", "resv", "resv", "sequence", "set", "char string"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$ //$NON-NLS-11$ //$NON-NLS-12$ //$NON-NLS-13$ //$NON-NLS-14$ //$NON-NLS-15$ //$NON-NLS-16$ //$NON-NLS-17$ //$NON-NLS-18$ //$NON-NLS-19$

	/**
	 * True if this is a structure for a constructed encoding.
	 */
	public boolean constructed;
	/**
	 * The tag type. Note that X.690 specifies encodings for tags with values greater than 31,
	 * but currently this class does not handle these kinds of tags.
	 */
	public byte tag;

	/**
	 * Constructs a BERProcessor to operate on the passed buffer. The first structure in the
	 * buffer will be processed before this method returns.
	 *
	 * @param buffer the buffer containing the BER structures.
	 * @param offset the offset into <code>buffer</code> to the start of the first structure.
	 * @param len the length of the BER structure.
	 * @throws SignatureException
	 */
	public BERProcessor(byte buffer[], int offset, int len) throws SignatureException {
		this.buffer = buffer;
		this.offset = offset;
		lastOffset = len + offset;
		processStructure();
	}

	/**
	 * Parse the structure found at the current <code>offset</code> into <code>buffer</code>.
	 * Most methods, constructor, and stepinto, will call this method automatically. If
	 * <code>offset</code> is modified outside of those methods, this method will need to
	 * be invoked.
	 */
	public void processStructure() throws SignatureException {
		// Don't process if we are at the end
		if (offset == -1)
			return;
		endOffset = offset;
		// section 8.1.2.2
		classOfTag = (buffer[offset] & 0xff) >> 6;
		// section 8.1.2.5
		constructed = (buffer[offset] & 0x20) != 0;
		// section 8.1.2.3
		byte tagNumber = (byte) (buffer[offset] & 0x1f);
		if (tagNumber < 32) {
			tag = tagNumber;
			endOffset = offset + 1;
		} else {
			throw new SignatureException("Can't handle tags > 32"); //$NON-NLS-1$
		}
		if ((buffer[endOffset] & 0x80) == 0) {
			// section 8.1.3.4 (doing the short form of the length)
			contentLength = buffer[endOffset];
			endOffset++;
		} else {
			// section 8.1.3.5 (doing the long form of the length)
			int octetCount = buffer[endOffset] & 0x7f;
			if (octetCount > 3)
				throw new SignatureException("ContentLength octet count too large: " + octetCount); //$NON-NLS-1$
			contentLength = 0;
			endOffset++;
			for (int i = 0; i < octetCount; i++) {
				contentLength <<= 8;
				contentLength |= buffer[endOffset] & 0xff;
				endOffset++;
			}
			// section 8.1.3.6 (doing the indefinite form
			if (octetCount == 0)
				contentLength = -1;
		}
		contentOffset = endOffset;
		if (contentLength != -1)
			endOffset += contentLength;
		if (endOffset > lastOffset)
			throw new SignatureException("Content length too large: " + endOffset + " > " + lastOffset); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Returns a String representation of the current BER structure.
	 * @return a String representation of the current BER structure.
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		switch (classOfTag) {
			case UNIVERSAL_TAGCLASS :
				sb.append('U');
				break;
			case APPLICATION_TAGCLASS :
				sb.append('A');
				break;
			case CONTEXTSPECIFIC_TAGCLASS :
				sb.append('C');
				break;
			case PRIVATE_TAGCLASS :
				sb.append('P');
				break;
		}
		sb.append(constructed ? 'C' : 'P');
		sb.append(" tag=" + tag); //$NON-NLS-1$
		if (tag < tagNames.length) {
			sb.append("(" + tagNames[tag] + ")"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		sb.append(" len="); //$NON-NLS-1$
		sb.append(contentLength);
		switch (tag) {
			case INTTAG :
				sb.append(" value=" + getIntValue()); //$NON-NLS-1$
				break;
			case OIDTAG :
				sb.append(" value="); //$NON-NLS-1$
				int oid[] = getObjId();
				for (int i = 0; i < oid.length; i++) {
					if (i > 0)
						sb.append('.');
					sb.append(oid[i]);
				}
		}
		if (tag == 12 || (tag >= 18 && tag <= 22) || (tag >= 25 && tag <= 30)) {
			sb.append(" value="); //$NON-NLS-1$
			sb.append(getString());
		}
		return sb.toString();
	}

	/**
	 * Returns a BERProcessor for the content of the current structure.
	 * @throws SignatureException
	 */
	public BERProcessor stepInto() throws SignatureException {
		return new BERProcessor(buffer, contentOffset, contentLength);
	}

	public void stepOver() throws SignatureException {
		offset = endOffset;
		if (endOffset >= lastOffset) {
			offset = -1;
			return;
		}
		processStructure();
	}

	public boolean endOfSequence() {
		return offset == -1;
	}

	/**
	 * Gets the content from the current structure as a String.
	 * @return the content from the current structure as a String.
	 */
	public String getString() {
		return new String(buffer, contentOffset, contentLength, StandardCharsets.UTF_8);
	}

	/**
	 * Gets the content from the current structure as an int.
	 * @return the content from the current structure as an int.
	 */
	public BigInteger getIntValue() {
		return new BigInteger(getBytes());
	}

	/**
	 * Gets the content from the current structure as an object id (int[]).
	 * @return the content from the current structure as an object id (int[]).
	 */
	public int[] getObjId() {
		// First count the ids
		int count = 0;
		for (int i = 0; i < contentLength; i++) {
			// section 8.19.2
			if ((buffer[contentOffset + i] & 0x80) == 0)
				count++;
		}
		count++; // section 8.19.3
		int oid[] = new int[count];
		int index = 0;
		int currentValue = 0;
		for (int i = 0; i < contentLength; i++) {
			currentValue <<= 7;
			currentValue |= buffer[contentOffset + i] & 0x7f;
			// section 8.19.2
			if ((buffer[contentOffset + i] & 0x80) == 0) {
				if (index == 0) {
					// section 8.19.4 special processing
					oid[index++] = currentValue / 40;
					oid[index++] = currentValue % 40;
				} else {
					oid[index++] = currentValue;
				}
				currentValue = 0;
			}
		}
		return oid;
	}

	/**
	 * Get a copy of the bytes in the content of the current structure.
	 * @return a copy of the bytes in the content of the current structure.
	 */
	public byte[] getBytes() {
		byte v[] = new byte[contentLength];
		System.arraycopy(buffer, contentOffset, v, 0, contentLength);
		return v;
	}

}
