blob: 980ab0567ba5b272d352166147282843d92a86a4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/**
* @author Alexander V. Esin, Stepan M. Mishura
* @version $Revision$
*/
package org.apache.harmony.security.x509;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.harmony.security.x501.AttributeTypeAndValue;
import org.apache.harmony.security.x501.AttributeValue;
/**
* Distinguished Name Parser.
*
* Parses a distinguished name(DN) string according
* BNF syntax specified in RFC 2253 and RFC 1779
*
* RFC 2253: Lightweight Directory Access Protocol (v3):
* UTF-8 String Representation of Distinguished Names
* http://www.ietf.org/rfc/rfc2253.txt
*
* RFC 1779: A String Representation of Distinguished Names
* http://www.ietf.org/rfc/rfc1779.txt
*/
public final class DNParser {
private int pos;
private int beg;
private int end;
/** distinguished name chars */
private final char[] chars;
/** raw string contains '"' or '\' */
private boolean hasQE;
/** DER encoding of currently parsed item */
private byte[] encoded;
/**
* @param dn - distinguished name string to be parsed
*/
public DNParser(String dn) throws IOException {
chars = dn.toCharArray();
}
/**
* Returns the next attribute type: (ALPHA 1*keychar) / oid
*/
private String nextAT() throws IOException {
hasQE = false; // reset
// skip preceding space chars, they can present after
// comma or semicolon (compatibility with RFC 1779)
for (; pos < chars.length && chars[pos] == ' '; pos++) {
}
if (pos == chars.length) {
return null; // reached the end of DN
}
// mark the beginning of attribute type
beg = pos;
// attribute type chars
pos++;
for (; pos < chars.length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
// we don't follow exact BNF syntax here:
// accept any char except space and '='
}
if (pos >= chars.length) {
// unexpected end of DN
throw new IOException("Invalid distinguished name string");
}
// mark the end of attribute type
end = pos;
// skip trailing space chars between attribute type and '='
// (compatibility with RFC 1779)
if (chars[pos] == ' ') {
for (; pos < chars.length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
}
if (chars[pos] != '=' || pos == chars.length) {
// unexpected end of DN
throw new IOException("Invalid distinguished name string");
}
}
pos++; //skip '=' char
// skip space chars between '=' and attribute value
// (compatibility with RFC 1779)
for (; pos < chars.length && chars[pos] == ' '; pos++) {
}
// in case of oid attribute type skip its prefix: "oid." or "OID."
// (compatibility with RFC 1779)
if ((end - beg > 4) && (chars[beg + 3] == '.')
&& (chars[beg] == 'O' || chars[beg] == 'o')
&& (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
&& (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
beg += 4;
}
return new String(chars, beg, end - beg);
}
/**
* Returns a quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
*/
private String quotedAV() throws IOException {
pos++;
beg = pos;
end = beg;
while (true) {
if (pos == chars.length) {
// unexpected end of DN
throw new IOException("Invalid distinguished name string");
}
if (chars[pos] == '"') {
// enclosing quotation was found
pos++;
break;
} else if (chars[pos] == '\\') {
chars[end] = getEscaped();
} else {
// shift char: required for string with escaped chars
chars[end] = chars[pos];
}
pos++;
end++;
}
// skip trailing space chars before comma or semicolon.
// (compatibility with RFC 1779)
for (; pos < chars.length && chars[pos] == ' '; pos++) {
}
return new String(chars, beg, end - beg);
}
/**
* Returns a hex string attribute value: "#" hexstring
*/
private String hexAV() throws IOException {
if (pos + 4 >= chars.length) {
// encoded byte array must be not less then 4 c
throw new IOException("Invalid distinguished name string");
}
beg = pos; // store '#' position
pos++;
while (true) {
// check for end of attribute value
// looks for space and component separators
if (pos == chars.length || chars[pos] == '+' || chars[pos] == ','
|| chars[pos] == ';') {
end = pos;
break;
}
if (chars[pos] == ' ') {
end = pos;
pos++;
// skip trailing space chars before comma or semicolon.
// (compatibility with RFC 1779)
for (; pos < chars.length && chars[pos] == ' '; pos++) {
}
break;
} else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
chars[pos] += 32; //to low case
}
pos++;
}
// verify length of hex string
// encoded byte array must be not less then 4 and must be even number
int hexLen = end - beg; // skip first '#' char
if (hexLen < 5 || (hexLen & 1) == 0) {
throw new IOException("Invalid distinguished name string");
}
// get byte encoding from string representation
encoded = new byte[hexLen / 2];
for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
encoded[i] = (byte) getByte(p);
}
return new String(chars, beg, hexLen);
}
/**
* Returns a string attribute value: *( stringchar / pair ).
*/
private String escapedAV() throws IOException {
beg = pos;
end = pos;
while (true) {
if (pos >= chars.length) {
// the end of DN has been found
return new String(chars, beg, end - beg);
}
switch (chars[pos]) {
case '+':
case ',':
case ';':
// separator char has beed found
return new String(chars, beg, end - beg);
case '\\':
// escaped char
chars[end++] = getEscaped();
pos++;
break;
case ' ':
// need to figure out whether space defines
// the end of attribute value or not
int cur = end;
pos++;
chars[end++] = ' ';
for (; pos < chars.length && chars[pos] == ' '; pos++) {
chars[end++] = ' ';
}
if (pos == chars.length || chars[pos] == ',' || chars[pos] == '+'
|| chars[pos] == ';') {
// separator char or the end of DN has beed found
return new String(chars, beg, cur - beg);
}
break;
default:
chars[end++] = chars[pos];
pos++;
}
}
}
/**
* Returns an escaped char
*/
private char getEscaped() throws IOException {
pos++;
if (pos == chars.length) {
throw new IOException("Invalid distinguished name string");
}
char ch = chars[pos];
switch (ch) {
case '"':
case '\\':
hasQE = true;
return ch;
case ',':
case '=':
case '+':
case '<':
case '>':
case '#':
case ';':
case ' ':
case '*':
case '%':
case '_':
//FIXME: escaping is allowed only for leading or trailing space char
return ch;
default:
// RFC doesn't explicitly say that escaped hex pair is
// interpreted as UTF-8 char. It only contains an example of such DN.
return getUTF8();
}
}
/**
* Decodes a UTF-8 char.
*/
protected char getUTF8() throws IOException {
int res = getByte(pos);
pos++; //FIXME tmp
if (res < 128) { // one byte: 0-7F
return (char) res;
} else if (res >= 192 && res <= 247) {
int count;
if (res <= 223) { // two bytes: C0-DF
count = 1;
res = res & 0x1F;
} else if (res <= 239) { // three bytes: E0-EF
count = 2;
res = res & 0x0F;
} else { // four bytes: F0-F7
count = 3;
res = res & 0x07;
}
int b;
for (int i = 0; i < count; i++) {
pos++;
if (pos == chars.length || chars[pos] != '\\') {
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
}
pos++;
b = getByte(pos);
pos++; //FIXME tmp
if ((b & 0xC0) != 0x80) {
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
}
res = (res << 6) + (b & 0x3F);
}
return (char) res;
} else {
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
}
}
/**
* Returns byte representation of a char pair
* The char pair is composed of DN char in
* specified 'position' and the next char
* According to BNF syntax:
* hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
* / "a" / "b" / "c" / "d" / "e" / "f"
*/
private int getByte(int position) throws IOException {
if ((position + 1) >= chars.length) {
// to avoid ArrayIndexOutOfBoundsException
throw new IOException("Invalid distinguished name string");
}
int b1 = chars[position];
if (b1 >= '0' && b1 <= '9') {
b1 = b1 - '0';
} else if (b1 >= 'a' && b1 <= 'f') {
b1 = b1 - 87; // 87 = 'a' - 10
} else if (b1 >= 'A' && b1 <= 'F') {
b1 = b1 - 55; // 55 = 'A' - 10
} else {
throw new IOException("Invalid distinguished name string");
}
int b2 = chars[position + 1];
if (b2 >= '0' && b2 <= '9') {
b2 = b2 - '0';
} else if (b2 >= 'a' && b2 <= 'f') {
b2 = b2 - 87; // 87 = 'a' - 10
} else if (b2 >= 'A' && b2 <= 'F') {
b2 = b2 - 55; // 55 = 'A' - 10
} else {
throw new IOException("Invalid distinguished name string");
}
return (b1 << 4) + b2;
}
/**
* Parses DN
*
* @return a list of Relative Distinguished Names(RND),
* each RDN is represented as a list of AttributeTypeAndValue objects
*/
public List<List<AttributeTypeAndValue>> parse() throws IOException {
List<List<AttributeTypeAndValue>> list = new ArrayList<List<AttributeTypeAndValue>>();
String attType = nextAT();
if (attType == null) {
return list; //empty list of RDNs
}
List<AttributeTypeAndValue> atav = new ArrayList<AttributeTypeAndValue>();
while (true) {
if (pos == chars.length) {
//empty Attribute Value
atav.add(new AttributeTypeAndValue(attType, new AttributeValue("", false)));
list.add(0, atav);
return list;
}
switch (chars[pos]) {
case '"':
atav.add(new AttributeTypeAndValue(attType, new AttributeValue(quotedAV(), hasQE)));
break;
case '#':
atav.add(new AttributeTypeAndValue(attType, new AttributeValue(hexAV(), encoded)));
break;
case '+':
case ',':
case ';': // compatibility with RFC 1779: semicolon can separate RDNs
//empty attribute value
atav.add(new AttributeTypeAndValue(attType, new AttributeValue("", false)));
break;
default:
atav.add(new AttributeTypeAndValue(attType, new AttributeValue(
escapedAV(), hasQE)));
}
if (pos >= chars.length) {
list.add(0, atav);
return list;
}
if (chars[pos] == ',' || chars[pos] == ';') {
list.add(0, atav);
atav = new ArrayList<AttributeTypeAndValue>();
} else if (chars[pos] != '+') {
throw new IOException("Invalid distinguished name string");
}
pos++;
attType = nextAT();
if (attType == null) {
throw new IOException("Invalid distinguished name string");
}
}
}
}