/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.asn1;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class provides a SAX-like parsing facility for DER-encoded data where elements of interest in the parse tree may
* be registered to handlers via the {@link #registerHandler} methods. {@link DERPath} strings are used to map handlers
* to elements of interest.
*
* @author Middleware Services
* @see DERPath
*/
public class DERParser
{
/** Logger for this class. */
protected final Logger logger = LoggerFactory.getLogger(getClass());
/** Handlers for DER paths. */
private final Map<DERPath, ParseHandler> handlerMap = new HashMap<>();
/** Permutations of the current path. */
private final Queue<DERPath> permutations = new ArrayDeque<>();
/**
* See {@link #registerHandler(DERPath, ParseHandler)}.
*
* @param path to register
* @param handler to associate with the path
*/
public void registerHandler(final String path, final ParseHandler handler)
{
registerHandler(new DERPath(path), handler);
}
/**
* Registers the supplied handler to fire when the supplied path is encountered.
*
* @param path to register
* @param handler to associate with the path
*/
public void registerHandler(final DERPath path, final ParseHandler handler)
{
handlerMap.put(path, handler);
}
/**
* Parse a DER-encoded data structure by calling registered handlers when points of interest are encountered in the
* parse tree.
*
* @param encoded DER-encoded bytes.
*/
public void parse(final ByteBuffer encoded)
{
parseTags(encoded);
}
/**
* Reads a DER tag from a single byte at the current position of the given buffer. The buffer position is naturally
* advanced one byte in this operation.
*
* @param encoded Buffer containing DER-encoded bytes positioned at tag.
*
* @return Tag or null if no universal tag or application-specific tag is known that matches the byte read in.
*/
public DERTag readTag(final ByteBuffer encoded)
{
if (encoded.position() >= encoded.limit()) {
return null;
}
DERTag tag;
final byte b = encoded.get();
// CheckStyle:MagicNumber OFF
final int tagNo = b & 0x1F;
final boolean constructed = (b & 0x20) == 0x20;
// Read class from first two high-order bits
switch (b & 0xC0) {
case UniversalDERTag.TAG_CLASS:
tag = UniversalDERTag.fromTagNo(tagNo);
break;
case ApplicationDERTag.TAG_CLASS:
tag = new ApplicationDERTag(tagNo, constructed);
break;
case ContextDERTag.TAG_CLASS:
tag = new ContextDERTag(tagNo, constructed);
break;
default:
// Private class (class 11b)
throw new IllegalArgumentException("Private classes not supported.");
}
// CheckStyle:MagicNumber ON
return tag;
}
/**
* Reads the length of a DER-encoded value from the given byte buffer. The buffer is expected to be positioned at the
* byte immediately following the tag byte, which is where the length byte(s) begin(s). Invocation of this method has
* two generally beneficial side effects:
*
* <ol>
* <li>Buffer is positioned at <em>start</em> of value bytes.</li>
* <li>Buffer limit is set to the <em>end</em> of value bytes.</li>
* </ol>
*
* @param encoded buffer containing DER-encoded bytes positioned at start of length byte(s).
*
* @return number of bytes occupied by tag value.
*/
public int readLength(final ByteBuffer encoded)
{
int length = 0;
final byte b = encoded.get();
// CheckStyle:MagicNumber OFF
if ((b & 0x80) == 0x80) {
final int len = b & 0x7F;
if (len > 0) {
encoded.limit(encoded.position() + len);
length = IntegerType.decodeUnsigned(encoded).intValue();
encoded.limit(encoded.capacity());
}
} else {
length = b;
}
return length;
// CheckStyle:MagicNumber ON
}
/**
* Reads the supplied DER encoded bytes and invokes handlers as configured paths are encountered.
*
* @param encoded to parse
*/
private void parseTags(final ByteBuffer encoded)
{
int index = 0;
while (encoded.position() < encoded.limit()) {
final DERTag tag = readTag(encoded);
if (tag != null) {
addTag(tag, index++);
parseTag(tag, encoded);
removeTag();
}
}
}
/**
* Invokes the parse handler for the current path and advances to the next position in the encoded bytes.
*
* @param tag to inspect for internal tags
* @param encoded to parse
*/
private void parseTag(final DERTag tag, final ByteBuffer encoded)
{
final int end = readLength(encoded) + encoded.position();
final int start = encoded.position();
// Invoke handlers for all permutations of current path
ParseHandler handler;
for (DERPath p : permutations) {
handler = handlerMap.get(p);
if (handler != null) {
encoded.position(start).limit(end);
handler.handle(this, encoded);
}
}
if (tag.isConstructed()) {
parseTags(encoded);
}
encoded.position(end).limit(encoded.capacity());
}
/**
* Add the given tag at the specified index to all permutations of the current parser path and increases the number of
* permutations as necessary to satisfy the following relation:
*
* <pre>size = 2^n</pre>
*
* <p>where n is the path length.</p>
*
* @param tag to add to path.
* @param index of tag relative to parent.
*/
private void addTag(final DERTag tag, final int index)
{
if (permutations.isEmpty()) {
permutations.add(new DERPath().pushNode(tag.name()));
permutations.add(new DERPath().pushNode(tag.name(), index));
} else {
final Collection<DERPath> generation = new ArrayDeque<>(permutations.size());
for (DERPath p : permutations) {
generation.add(new DERPath(p).pushNode(tag.name()));
p.pushNode(tag.name(), index);
}
permutations.addAll(generation);
}
}
/**
* Removes the tag at the leaf position of all permutations of the current parser path, and reduces the number of
* permutations as necessary to satisfy the following relation:
*
* <pre>size = 2^n</pre>
*
* <p>where n is the path length.</p>
*/
private void removeTag()
{
final int half = permutations.size() / 2;
while (permutations.size() > half) {
permutations.remove();
}
permutations.forEach(DERPath::popNode);
}
}