package model.x501;
import annotations.Assoc;
import model.asn1.*;
import model.asn1.exceptions.ParseException;
import model.asn1.parsing.BytesReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Represents an X.501 directory Name (a.k.a. RDNSequence).
*
* Name ::= CHOICE { -- only one possibility for now -- rdnSequence RDNSequence }
* RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
* DistinguishedName ::= RDNSequence
*
*/
public class Name extends ASN1Object {
@Assoc(partOf = true)
private final RelativeDistinguishedName[] rdnSequence;
/**
* EFFECT: Initialize the Name with the given tags and rdnSequence. For tag and parentTag, consult
* {@link ASN1Object}.
* REQUIRES: Items should have SET tag.
*/
public Name(Tag tag, Tag parentTag, RelativeDistinguishedName[] rdnSequence) {
super(tag, parentTag);
this.rdnSequence = rdnSequence;
}
/**
* EFFECT: Parse the Name from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
* Throws {@link ParseException} for invalid input.
* MODIFIES: this, encoded
*/
public Name(BytesReader encoded, boolean hasParentTag) throws ParseException {
super(encoded, hasParentTag);
final List list = new ArrayList<>();
for (int i = 0; i < getLength(); ) {
int index = encoded.getIndex();
final RelativeDistinguishedName name = new RelativeDistinguishedName(encoded, false);
name.getTag().enforce(TAG_SET);
list.add(name);
index = encoded.getIndex() - index;
i += index;
}
this.rdnSequence = list.toArray(new RelativeDistinguishedName[0]);
}
/**
* EFFECTS: Parse OID after last KV and clear context if input is '='. Otherwise add to context.
* Throws {@link ParseException} if input is '+' or ',', or if the oid cannot be recognized.
* MODIFIES: context
*/
private static ObjectIdentifier handleKey(char c, List context) throws ParseException {
if (c == '=') {
if (context.isEmpty()) {
throw new ParseException("Unterminated key");
}
final ObjectIdentifier oid = new ObjectIdentifier(ObjectIdentifier.TAG, null,
ObjectIdentifier.getKnown(
context.stream().map(Object::toString).collect(Collectors.joining(""))));
context.clear();
return oid;
} else if (c == '+' || c == ',') {
throw new ParseException("Unterminated key part: " + context);
} else {
context.add(c);
return null;
}
}
/**
* EFFECTS: Parse KV after '='. Clear context.
* Throws {@link ParseException} if context is empty.
* MODIFIES: context
* REQUIRES: curKey to be a valid OID
*/
private static AttributeTypeAndValue flushKV(ObjectIdentifier curKey, List context)
throws ParseException {
if (context.isEmpty()) {
throw new ParseException("Unterminated value");
}
final AttributeTypeAndValue tv = new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, curKey,
new PrintableString(PrintableString.TAG, null,
context.stream().map(Object::toString).collect(Collectors.joining(""))));
context.clear();
return tv;
}
/**
* EFFECTS: Handle value after =, optionally flush to rdns after ',', or add to curListKT if after '+'. Clears
* context if flushed, otherwise add to context. Returns whether switch to the state of reading key.
* Throws {@link ParseException} if c is '=' or context is empty.
* MODIFIES: context, curListKT, rdns
* REQUIRES: curKey to be a valid OID
*/
private static boolean handleValue(char c, List context, List curListKT,
ObjectIdentifier curKey, List rdns)
throws ParseException {
if (c == ',') {
if (context.isEmpty()) {
throw new ParseException("Unterminated value");
}
curListKT.add(flushKV(curKey, context));
rdns.add(new RelativeDistinguishedName(ASN1Object.TAG_SET, null,
curListKT.toArray(AttributeTypeAndValue[]::new)));
curListKT.clear();
return true;
} else if (c == '+') {
curListKT.add(flushKV(curKey, context));
return true;
} else if (c == '=') {
throw new ParseException("Unterminated value part: " + context);
} else {
context.add(c);
return false;
}
}
/**
* EFFECTS: Parse the given DN string into structural X.509 RDN Sequence.
* Character literals = + , must be escaped.
* Values will always be PrintableString.
* Throws {@link ParseException} if invalid.
*/
public static Name parseString(String dn) throws ParseException {
char state = 0; // 0 - Key, 1 - Value; MSB: Escaped
List rdns = new ArrayList<>();
List curListKT = new ArrayList<>();
ObjectIdentifier curKey = null;
List context = new ArrayList<>();
for (char c : (dn + ",").toCharArray()) {
if ((state >> 7) == 1) {
context.add(c);
state &= 127;
continue;
} else if (c == '\\') {
state |= 128;
continue;
}
if (state == 0) {
if ((curKey = handleKey(c, context)) != null) {
state = 1;
}
} else if (handleValue(c, context, curListKT, curKey, rdns)) {
state = 0;
}
}
return new Name(ASN1Object.TAG_SEQUENCE, null, rdns.toArray(RelativeDistinguishedName[]::new));
}
/**
* EFFECTS: Encode the SEQUENCE OF into DER, keep order. RDNs will be encoded one-by-one.
*/
@Override
public Byte[] encodeValueDER() {
return Stream.of(rdnSequence)
.map(Encodable::encodeDER)
.flatMap(Arrays::stream)
.toArray(Byte[]::new);
}
/**
* EFFECT: Convert the name into directory string, like CN=yuuta,OU=users,DC=yuuta,DC=moe
*/
@Override
public String toString() {
return Stream.of(rdnSequence)
.map(RelativeDistinguishedName::toString)
.collect(Collectors.joining(","));
}
public RelativeDistinguishedName[] getRdnSequence() {
return rdnSequence;
}
}