Home | History | Annotate | Download | only in x501
      1 /*
      2  *  Licensed to the Apache Software Foundation (ASF) under one or more
      3  *  contributor license agreements.  See the NOTICE file distributed with
      4  *  this work for additional information regarding copyright ownership.
      5  *  The ASF licenses this file to You under the Apache License, Version 2.0
      6  *  (the "License"); you may not use this file except in compliance with
      7  *  the License.  You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *  Unless required by applicable law or agreed to in writing, software
     12  *  distributed under the License is distributed on an "AS IS" BASIS,
     13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *  See the License for the specific language governing permissions and
     15  *  limitations under the License.
     16  */
     17 
     18 /**
     19 * @author Alexander V. Esin
     20 * @version $Revision$
     21 */
     22 
     23 package org.apache.harmony.security.x501;
     24 
     25 import java.io.IOException;
     26 import java.util.Collection;
     27 import org.apache.harmony.security.asn1.ASN1SetOf;
     28 import org.apache.harmony.security.asn1.ASN1StringType;
     29 import org.apache.harmony.security.asn1.ASN1Type;
     30 import org.apache.harmony.security.asn1.DerInputStream;
     31 import org.apache.harmony.security.utils.ObjectIdentifier;
     32 
     33 /**
     34  * X.501 Attribute Value
     35  */
     36 public final class AttributeValue {
     37 
     38     public boolean wasEncoded;
     39 
     40     private boolean hasConsecutiveSpaces;
     41 
     42     public final String escapedString;
     43 
     44     private String rfc2253String;
     45 
     46     private String hexString;
     47 
     48     private final int tag;
     49 
     50     public byte[] encoded;
     51 
     52     public byte[] bytes; //FIXME remove??? bytes to be encoded
     53 
     54     public boolean hasQE; // raw string contains '"' or '\'
     55 
     56     public final String rawString;
     57 
     58     public AttributeValue(String parsedString, boolean hasQorE, ObjectIdentifier oid) {
     59         wasEncoded = false;
     60 
     61         this.hasQE = hasQorE;
     62         this.rawString = parsedString;
     63         this.escapedString = makeEscaped(rawString); // overwrites hasQE
     64 
     65         int tag;
     66         if (oid == AttributeTypeAndValue.EMAILADDRESS || oid == AttributeTypeAndValue.DC) {
     67             // http://www.rfc-editor.org/rfc/rfc5280.txt
     68             // says that EmailAddress and DomainComponent should be a IA5String
     69             tag = ASN1StringType.IA5STRING.id;
     70         } else if (isPrintableString(rawString)) {
     71             tag = ASN1StringType.PRINTABLESTRING.id;
     72         } else {
     73             tag = ASN1StringType.UTF8STRING.id;
     74         }
     75         this.tag = tag;
     76     }
     77 
     78     public AttributeValue(String hexString, byte[] encoded) {
     79         wasEncoded = true;
     80 
     81         this.hexString = hexString;
     82         this.encoded = encoded;
     83 
     84         try {
     85             DerInputStream in = new DerInputStream(encoded);
     86 
     87             tag = in.tag;
     88 
     89             if (DirectoryString.ASN1.checkTag(tag)) {
     90                 // has string representation
     91                 this.rawString = (String) DirectoryString.ASN1.decode(in);
     92                 this.escapedString = makeEscaped(rawString);
     93             } else {
     94                 this.rawString = hexString;
     95                 this.escapedString = hexString;
     96             }
     97         } catch (IOException e) {
     98             IllegalArgumentException iae = new IllegalArgumentException(); //FIXME message
     99             iae.initCause(e);
    100             throw iae;
    101         }
    102     }
    103 
    104     public AttributeValue(String rawString, byte[] encoded, int tag) {
    105         wasEncoded = true;
    106 
    107         this.encoded = encoded;
    108         this.tag = tag;
    109 
    110         if (rawString == null) {
    111             this.rawString = getHexString();
    112             this.escapedString = hexString;
    113         } else {
    114             this.rawString = rawString;
    115             this.escapedString = makeEscaped(rawString);
    116         }
    117     }
    118 
    119     /**
    120      * Checks if the string is PrintableString (see X.680)
    121      */
    122     private static boolean isPrintableString(String str) {
    123         for (int i = 0; i< str.length(); ++i) {
    124             char ch = str.charAt(i);
    125             if (!(ch == 0x20
    126             || ch >= 0x27 && ch<= 0x29 // '()
    127             || ch >= 0x2B && ch<= 0x3A // +,-./0-9:
    128             || ch == '='
    129             || ch == '?'
    130             || ch >= 'A' && ch<= 'Z'
    131             || ch >= 'a' && ch<= 'z')) {
    132                 return false;
    133             }
    134         }
    135         return true;
    136     }
    137 
    138     public int getTag() {
    139         return tag;
    140     }
    141 
    142     public String getHexString() {
    143         if (hexString == null) {
    144             if (!wasEncoded) {
    145                 //FIXME optimize me: what about reusable OutputStream???
    146                 if (tag == ASN1StringType.IA5STRING.id) {
    147                     encoded = ASN1StringType.IA5STRING.encode(rawString);
    148                 } else if (tag == ASN1StringType.PRINTABLESTRING.id) {
    149                     encoded = ASN1StringType.PRINTABLESTRING.encode(rawString);
    150                 } else {
    151                     encoded = ASN1StringType.UTF8STRING.encode(rawString);
    152                 }
    153                 wasEncoded = true;
    154             }
    155 
    156             StringBuilder buf = new StringBuilder(encoded.length * 2 + 1);
    157             buf.append('#');
    158 
    159             for (int i = 0, c; i < encoded.length; i++) {
    160                 c = (encoded[i] >> 4) & 0x0F;
    161                 if (c < 10) {
    162                     buf.append((char) (c + 48));
    163                 } else {
    164                     buf.append((char) (c + 87));
    165                 }
    166 
    167                 c = encoded[i] & 0x0F;
    168                 if (c < 10) {
    169                     buf.append((char) (c + 48));
    170                 } else {
    171                     buf.append((char) (c + 87));
    172                 }
    173             }
    174             hexString = buf.toString();
    175         }
    176         return hexString;
    177     }
    178 
    179     public Collection<?> getValues(ASN1Type type) throws IOException {
    180         return (Collection<?>) new ASN1SetOf(type).decode(encoded);
    181     }
    182 
    183     public void appendQEString(StringBuilder sb) {
    184         sb.append('"');
    185         if (hasQE) {
    186             char c;
    187             for (int i = 0; i < rawString.length(); i++) {
    188                 c = rawString.charAt(i);
    189                 if (c == '"' || c == '\\') {
    190                     sb.append('\\');
    191                 }
    192                 sb.append(c);
    193             }
    194         } else {
    195             sb.append(rawString);
    196         }
    197         sb.append('"');
    198     }
    199 
    200     /**
    201      * Escapes:
    202      * 1) chars ",", "+", """, "\", "<", ">", ";" (RFC 2253)
    203      * 2) chars "#", "=" (required by RFC 1779)
    204      * 3) leading or trailing spaces
    205      * 4) consecutive spaces (RFC 1779)
    206      * 5) according to the requirement to be RFC 1779 compatible:
    207      *    '#' char is escaped in any position
    208      */
    209     private String makeEscaped(String name) {
    210         int length = name.length();
    211         if (length == 0) {
    212             return name;
    213         }
    214         StringBuilder buf = new StringBuilder(length * 2);
    215 
    216         // Keeps track of whether we are escaping spaces.
    217         boolean escapeSpaces = false;
    218 
    219         for (int index = 0; index < length; index++) {
    220             char ch = name.charAt(index);
    221             switch (ch) {
    222             case ' ':
    223                 /*
    224                  * We should escape spaces in the following cases:
    225                  *   1) at the beginning
    226                  *   2) at the end
    227                  *   3) consecutive spaces
    228                  * Since multiple spaces at the beginning or end will be covered by
    229                  * 3, we don't need a special case to check for that. Note that RFC 2253
    230                  * doesn't escape consecutive spaces, so they are removed in
    231                  * getRFC2253String instead of making two different strings here.
    232                  */
    233                 if (index < (length - 1)) {
    234                     boolean nextIsSpace = name.charAt(index + 1) == ' ';
    235                     escapeSpaces = escapeSpaces || nextIsSpace || index == 0;
    236                     hasConsecutiveSpaces |= nextIsSpace;
    237                 } else {
    238                     escapeSpaces = true;
    239                 }
    240 
    241                 if (escapeSpaces) {
    242                     buf.append('\\');
    243                 }
    244 
    245                 buf.append(' ');
    246                 break;
    247 
    248             case '"':
    249             case '\\':
    250                 hasQE = true;
    251                 buf.append('\\');
    252                 buf.append(ch);
    253                 break;
    254 
    255             case ',':
    256             case '+':
    257             case '<':
    258             case '>':
    259             case ';':
    260             case '#': // required by RFC 1779
    261             case '=': // required by RFC 1779
    262                 buf.append('\\');
    263                 buf.append(ch);
    264                 break;
    265 
    266             default:
    267                 buf.append(ch);
    268                 break;
    269             }
    270 
    271             if (escapeSpaces && ch != ' ') {
    272                 escapeSpaces = false;
    273             }
    274         }
    275 
    276         return buf.toString();
    277     }
    278 
    279     public String makeCanonical() {
    280         int length = rawString.length();
    281         if (length == 0) {
    282             return rawString;
    283         }
    284         StringBuilder buf = new StringBuilder(length * 2);
    285 
    286         int index = 0;
    287         if (rawString.charAt(0) == '#') {
    288             buf.append('\\');
    289             buf.append('#');
    290             index++;
    291         }
    292 
    293         int bufLength;
    294         for (; index < length; index++) {
    295             char ch = rawString.charAt(index);
    296 
    297             switch (ch) {
    298             case ' ':
    299                 bufLength = buf.length();
    300                 if (bufLength == 0 || buf.charAt(bufLength - 1) == ' ') {
    301                     break;
    302                 }
    303                 buf.append(' ');
    304                 break;
    305 
    306             case '"':
    307             case '\\':
    308             case ',':
    309             case '+':
    310             case '<':
    311             case '>':
    312             case ';':
    313                 buf.append('\\');
    314 
    315             default:
    316                 buf.append(ch);
    317             }
    318         }
    319 
    320         //remove trailing spaces
    321         for (bufLength = buf.length() - 1; bufLength > -1
    322                 && buf.charAt(bufLength) == ' '; bufLength--) {
    323         }
    324         buf.setLength(bufLength + 1);
    325 
    326         return buf.toString();
    327     }
    328 
    329     /**
    330      * Removes escape sequences used in RFC1779 escaping but not in RFC2253 and
    331      * returns the RFC2253 string to the caller..
    332      */
    333     public String getRFC2253String() {
    334         if (!hasConsecutiveSpaces) {
    335             return escapedString;
    336         }
    337 
    338         if (rfc2253String == null) {
    339             // Scan backwards first since runs of spaces at the end are escaped.
    340             int lastIndex = escapedString.length() - 2;
    341             for (int i = lastIndex; i > 0; i -= 2) {
    342                 if (escapedString.charAt(i) == '\\' && escapedString.charAt(i + 1) == ' ') {
    343                     lastIndex = i - 1;
    344                 }
    345             }
    346 
    347             boolean beginning = true;
    348             StringBuilder sb = new StringBuilder(escapedString.length());
    349             for (int i = 0; i < escapedString.length(); i++) {
    350                 char ch = escapedString.charAt(i);
    351                 if (ch != '\\') {
    352                     sb.append(ch);
    353                     beginning = false;
    354                 } else {
    355                     char nextCh = escapedString.charAt(i + 1);
    356                     if (nextCh == ' ') {
    357                         if (beginning || i > lastIndex) {
    358                             sb.append(ch);
    359                         }
    360                         sb.append(nextCh);
    361                     } else {
    362                         sb.append(ch);
    363                         sb.append(nextCh);
    364                         beginning = false;
    365                     }
    366 
    367                     i++;
    368                 }
    369             }
    370             rfc2253String = sb.toString();
    371         }
    372         return rfc2253String;
    373     }
    374 }
    375