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