1 /* 2 * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.jndi.ldap; 27 28 29 import java.util.Enumeration; 30 import java.util.Vector; 31 import java.util.Locale; 32 33 import javax.naming.*; 34 import javax.naming.directory.Attributes; 35 import javax.naming.directory.Attribute; 36 import javax.naming.directory.BasicAttributes; 37 38 39 /** 40 * <code>LdapName</code> implements compound names for LDAP v3 as 41 * specified by RFC 2253. 42 *<p> 43 * RFC 2253 has a few ambiguities and outright inconsistencies. These 44 * are resolved as follows: 45 * <ul> 46 * <li> RFC 2253 leaves the term "whitespace" undefined. The 47 * definition of "optional-space" given in RFC 1779 is used in 48 * its place: either a space character or a carriage return ("\r"). 49 * <li> Whitespace is allowed on either side of ',', ';', '=', and '+'. 50 * Such whitespace is accepted but not generated by this code, 51 * and is ignored when comparing names. 52 * <li> AttributeValue strings containing '=' or non-leading '#' 53 * characters (unescaped) are accepted. 54 * </ul> 55 *<p> 56 * String names passed to <code>LdapName</code> or returned by it 57 * use the full 16-bit Unicode character set. They may also contain 58 * characters encoded into UTF-8 with each octet represented by a 59 * three-character substring such as "\\B4". 60 * They may not, however, contain characters encoded into UTF-8 with 61 * each octet represented by a single character in the string: the 62 * meaning would be ambiguous. 63 *<p> 64 * <code>LdapName</code> will properly parse all valid names, but 65 * does not attempt to detect all possible violations when parsing 66 * invalid names. It's "generous". 67 *<p> 68 * When names are tested for equality, attribute types and binary 69 * values are case-insensitive, and string values are by default 70 * case-insensitive. 71 * String values with different but equivalent usage of quoting, 72 * escaping, or UTF8-hex-encoding are considered equal. The order of 73 * components in multi-valued RDNs (such as "ou=Sales+cn=Bob") is not 74 * significant. 75 * 76 * @author Scott Seligman 77 */ 78 79 public final class LdapName implements Name { 80 81 private transient String unparsed; // if non-null, the DN in unparsed form 82 private transient Vector<Rdn> rdns; // parsed name components 83 private transient boolean valuesCaseSensitive = false; 84 85 /** 86 * Constructs an LDAP name from the given DN. 87 * 88 * @param name An LDAP DN. To JNDI, a compound name. 89 * 90 * @throws InvalidNameException if a syntax violation is detected. 91 */ 92 public LdapName(String name) throws InvalidNameException { 93 unparsed = name; 94 parse(); 95 } 96 97 /* 98 * Constructs an LDAP name given its parsed components and, optionally 99 * (if "name" is not null), the unparsed DN. 100 */ 101 @SuppressWarnings("unchecked") // clone() 102 private LdapName(String name, Vector<Rdn> rdns) { 103 unparsed = name; 104 this.rdns = (Vector<Rdn>)rdns.clone(); 105 } 106 107 /* 108 * Constructs an LDAP name given its parsed components (the elements 109 * of "rdns" in the range [beg,end)) and, optionally 110 * (if "name" is not null), the unparsed DN. 111 */ 112 private LdapName(String name, Vector<Rdn> rdns, int beg, int end) { 113 unparsed = name; 114 this.rdns = new Vector<>(); 115 for (int i = beg; i < end; i++) { 116 this.rdns.addElement(rdns.elementAt(i)); 117 } 118 } 119 120 121 public Object clone() { 122 return new LdapName(unparsed, rdns); 123 } 124 125 public String toString() { 126 if (unparsed != null) { 127 return unparsed; 128 } 129 130 StringBuffer buf = new StringBuffer(); 131 for (int i = rdns.size() - 1; i >= 0; i--) { 132 if (i < rdns.size() - 1) { 133 buf.append(','); 134 } 135 Rdn rdn = rdns.elementAt(i); 136 buf.append(rdn); 137 } 138 139 unparsed = new String(buf); 140 return unparsed; 141 } 142 143 public boolean equals(Object obj) { 144 return ((obj instanceof LdapName) && 145 (compareTo(obj) == 0)); 146 } 147 148 public int compareTo(Object obj) { 149 LdapName that = (LdapName)obj; 150 151 if ((obj == this) || // check possible shortcuts 152 (unparsed != null && unparsed.equals(that.unparsed))) { 153 return 0; 154 } 155 156 // Compare RDNs one by one, lexicographically. 157 int minSize = Math.min(rdns.size(), that.rdns.size()); 158 for (int i = 0 ; i < minSize; i++) { 159 // Compare a single pair of RDNs. 160 Rdn rdn1 = rdns.elementAt(i); 161 Rdn rdn2 = that.rdns.elementAt(i); 162 163 int diff = rdn1.compareTo(rdn2); 164 if (diff != 0) { 165 return diff; 166 } 167 } 168 return (rdns.size() - that.rdns.size()); // longer DN wins 169 } 170 171 public int hashCode() { 172 // Sum up the hash codes of the components. 173 int hash = 0; 174 175 // For each RDN... 176 for (int i = 0; i < rdns.size(); i++) { 177 Rdn rdn = rdns.elementAt(i); 178 hash += rdn.hashCode(); 179 } 180 return hash; 181 } 182 183 public int size() { 184 return rdns.size(); 185 } 186 187 public boolean isEmpty() { 188 return rdns.isEmpty(); 189 } 190 191 public Enumeration<String> getAll() { 192 final Enumeration<Rdn> enum_ = rdns.elements(); 193 194 return new Enumeration<String>() { 195 public boolean hasMoreElements() { 196 return enum_.hasMoreElements(); 197 } 198 public String nextElement() { 199 return enum_.nextElement().toString(); 200 } 201 }; 202 } 203 204 public String get(int pos) { 205 return rdns.elementAt(pos).toString(); 206 } 207 208 public Name getPrefix(int pos) { 209 return new LdapName(null, rdns, 0, pos); 210 } 211 212 public Name getSuffix(int pos) { 213 return new LdapName(null, rdns, pos, rdns.size()); 214 } 215 216 public boolean startsWith(Name n) { 217 int len1 = rdns.size(); 218 int len2 = n.size(); 219 return (len1 >= len2 && 220 matches(0, len2, n)); 221 } 222 223 public boolean endsWith(Name n) { 224 int len1 = rdns.size(); 225 int len2 = n.size(); 226 return (len1 >= len2 && 227 matches(len1 - len2, len1, n)); 228 } 229 230 /** 231 * Controls whether string-values are treated as case-sensitive 232 * when the string values within names are compared. The default 233 * behavior is case-insensitive comparison. 234 */ 235 public void setValuesCaseSensitive(boolean caseSensitive) { 236 toString(); 237 rdns = null; // clear any cached information 238 try { 239 parse(); 240 } catch (InvalidNameException e) { 241 // shouldn't happen 242 throw new IllegalStateException("Cannot parse name: " + unparsed); 243 } 244 valuesCaseSensitive = caseSensitive; 245 } 246 247 /* 248 * Helper method for startsWith() and endsWith(). 249 * Returns true if components [beg,end) match the components of "n". 250 * If "n" is not an LdapName, each of its components is parsed as 251 * the string form of an RDN. 252 * The following must hold: end - beg == n.size(). 253 */ 254 private boolean matches(int beg, int end, Name n) { 255 for (int i = beg; i < end; i++) { 256 Rdn rdn; 257 if (n instanceof LdapName) { 258 LdapName ln = (LdapName)n; 259 rdn = ln.rdns.elementAt(i - beg); 260 } else { 261 String rdnString = n.get(i - beg); 262 try { 263 rdn = (new DnParser(rdnString, valuesCaseSensitive)).getRdn(); 264 } catch (InvalidNameException e) { 265 return false; 266 } 267 } 268 269 if (!rdn.equals(rdns.elementAt(i))) { 270 return false; 271 } 272 } 273 return true; 274 } 275 276 public Name addAll(Name suffix) throws InvalidNameException { 277 return addAll(size(), suffix); 278 } 279 280 /* 281 * If "suffix" is not an LdapName, each of its components is parsed as 282 * the string form of an RDN. 283 */ 284 public Name addAll(int pos, Name suffix) throws InvalidNameException { 285 if (suffix instanceof LdapName) { 286 LdapName s = (LdapName)suffix; 287 for (int i = 0; i < s.rdns.size(); i++) { 288 rdns.insertElementAt(s.rdns.elementAt(i), pos++); 289 } 290 } else { 291 Enumeration<String> comps = suffix.getAll(); 292 while (comps.hasMoreElements()) { 293 DnParser p = new DnParser(comps.nextElement(), 294 valuesCaseSensitive); 295 rdns.insertElementAt(p.getRdn(), pos++); 296 } 297 } 298 unparsed = null; // no longer valid 299 return this; 300 } 301 302 public Name add(String comp) throws InvalidNameException { 303 return add(size(), comp); 304 } 305 306 public Name add(int pos, String comp) throws InvalidNameException { 307 Rdn rdn = (new DnParser(comp, valuesCaseSensitive)).getRdn(); 308 rdns.insertElementAt(rdn, pos); 309 unparsed = null; // no longer valid 310 return this; 311 } 312 313 public Object remove(int pos) throws InvalidNameException { 314 String comp = get(pos); 315 rdns.removeElementAt(pos); 316 unparsed = null; // no longer valid 317 return comp; 318 } 319 320 321 private void parse() throws InvalidNameException { 322 rdns = (new DnParser(unparsed, valuesCaseSensitive)).getDn(); 323 } 324 325 /* 326 * Best guess as to what RFC 2253 means by "whitespace". 327 */ 328 private static boolean isWhitespace(char c) { 329 return (c == ' ' || c == '\r'); 330 } 331 332 /** 333 * Given the value of an attribute, returns a string suitable 334 * for inclusion in a DN. If the value is a string, this is 335 * accomplished by using backslash (\) to escape the following 336 * characters: 337 *<ul> 338 *<li>leading and trailing whitespace 339 *<li><pre>, = + < > # ; " \</pre> 340 *</ul> 341 * If the value is a byte array, it is converted to hex 342 * notation (such as "#CEB1DF80"). 343 */ 344 public static String escapeAttributeValue(Object val) { 345 return TypeAndValue.escapeValue(val); 346 } 347 348 /** 349 * Given an attribute value formatted according to RFC 2253, 350 * returns the unformatted value. Returns a string value as 351 * a string, and a binary value as a byte array. 352 */ 353 public static Object unescapeAttributeValue(String val) { 354 return TypeAndValue.unescapeValue(val); 355 } 356 357 /** 358 * Serializes only the unparsed DN, for compactness and to avoid 359 * any implementation dependency. 360 * 361 * @serialdata The DN string and a boolean indicating whether 362 * the values are case sensitive. 363 */ 364 private void writeObject(java.io.ObjectOutputStream s) 365 throws java.io.IOException { 366 s.writeObject(toString()); 367 s.writeBoolean(valuesCaseSensitive); 368 } 369 370 private void readObject(java.io.ObjectInputStream s) 371 throws java.io.IOException, ClassNotFoundException { 372 unparsed = (String)s.readObject(); 373 valuesCaseSensitive = s.readBoolean(); 374 try { 375 parse(); 376 } catch (InvalidNameException e) { 377 // shouldn't happen 378 throw new java.io.StreamCorruptedException( 379 "Invalid name: " + unparsed); 380 } 381 } 382 383 static final long serialVersionUID = -1595520034788997356L; 384 385 386 /* 387 * DnParser implements a recursive descent parser for a single DN. 388 */ 389 static class DnParser { 390 391 private final String name; // DN being parsed 392 private final char[] chars; // characters in LDAP name being parsed 393 private final int len; // length of "chars" 394 private int cur = 0; // index of first unconsumed char in "chars" 395 private boolean valuesCaseSensitive; 396 397 /* 398 * Given an LDAP DN in string form, returns a parser for it. 399 */ 400 DnParser(String name, boolean valuesCaseSensitive) 401 throws InvalidNameException { 402 this.name = name; 403 len = name.length(); 404 chars = name.toCharArray(); 405 this.valuesCaseSensitive = valuesCaseSensitive; 406 } 407 408 /* 409 * Parses the DN, returning a Vector of its RDNs. 410 */ 411 Vector<Rdn> getDn() throws InvalidNameException { 412 cur = 0; 413 Vector<Rdn> rdns = new Vector<>(len / 3 + 10); // leave room for growth 414 415 if (len == 0) { 416 return rdns; 417 } 418 419 rdns.addElement(parseRdn()); 420 while (cur < len) { 421 if (chars[cur] == ',' || chars[cur] == ';') { 422 ++cur; 423 rdns.insertElementAt(parseRdn(), 0); 424 } else { 425 throw new InvalidNameException("Invalid name: " + name); 426 } 427 } 428 return rdns; 429 } 430 431 /* 432 * Parses the DN, if it is known to contain a single RDN. 433 */ 434 Rdn getRdn() throws InvalidNameException { 435 Rdn rdn = parseRdn(); 436 if (cur < len) { 437 throw new InvalidNameException("Invalid RDN: " + name); 438 } 439 return rdn; 440 } 441 442 /* 443 * Parses the next RDN and returns it. Throws an exception if 444 * none is found. Leading and trailing whitespace is consumed. 445 */ 446 private Rdn parseRdn() throws InvalidNameException { 447 448 Rdn rdn = new Rdn(); 449 while (cur < len) { 450 consumeWhitespace(); 451 String attrType = parseAttrType(); 452 consumeWhitespace(); 453 if (cur >= len || chars[cur] != '=') { 454 throw new InvalidNameException("Invalid name: " + name); 455 } 456 ++cur; // consume '=' 457 consumeWhitespace(); 458 String value = parseAttrValue(); 459 consumeWhitespace(); 460 461 rdn.add(new TypeAndValue(attrType, value, valuesCaseSensitive)); 462 if (cur >= len || chars[cur] != '+') { 463 break; 464 } 465 ++cur; // consume '+' 466 } 467 return rdn; 468 } 469 470 /* 471 * Returns the attribute type that begins at the next unconsumed 472 * char. No leading whitespace is expected. 473 * This routine is more generous than RFC 2253. It accepts 474 * attribute types composed of any nonempty combination of Unicode 475 * letters, Unicode digits, '.', '-', and internal space characters. 476 */ 477 private String parseAttrType() throws InvalidNameException { 478 479 final int beg = cur; 480 while (cur < len) { 481 char c = chars[cur]; 482 if (Character.isLetterOrDigit(c) || 483 c == '.' || 484 c == '-' || 485 c == ' ') { 486 ++cur; 487 } else { 488 break; 489 } 490 } 491 // Back out any trailing spaces. 492 while ((cur > beg) && (chars[cur - 1] == ' ')) { 493 --cur; 494 } 495 496 if (beg == cur) { 497 throw new InvalidNameException("Invalid name: " + name); 498 } 499 return new String(chars, beg, cur - beg); 500 } 501 502 /* 503 * Returns the attribute value that begins at the next unconsumed 504 * char. No leading whitespace is expected. 505 */ 506 private String parseAttrValue() throws InvalidNameException { 507 508 if (cur < len && chars[cur] == '#') { 509 return parseBinaryAttrValue(); 510 } else if (cur < len && chars[cur] == '"') { 511 return parseQuotedAttrValue(); 512 } else { 513 return parseStringAttrValue(); 514 } 515 } 516 517 private String parseBinaryAttrValue() throws InvalidNameException { 518 final int beg = cur; 519 ++cur; // consume '#' 520 while (cur < len && 521 Character.isLetterOrDigit(chars[cur])) { 522 ++cur; 523 } 524 return new String(chars, beg, cur - beg); 525 } 526 527 private String parseQuotedAttrValue() throws InvalidNameException { 528 529 final int beg = cur; 530 ++cur; // consume '"' 531 532 while ((cur < len) && chars[cur] != '"') { 533 if (chars[cur] == '\\') { 534 ++cur; // consume backslash, then what follows 535 } 536 ++cur; 537 } 538 if (cur >= len) { // no closing quote 539 throw new InvalidNameException("Invalid name: " + name); 540 } 541 ++cur ; // consume closing quote 542 543 return new String(chars, beg, cur - beg); 544 } 545 546 private String parseStringAttrValue() throws InvalidNameException { 547 548 final int beg = cur; 549 int esc = -1; // index of the most recently escaped character 550 551 while ((cur < len) && !atTerminator()) { 552 if (chars[cur] == '\\') { 553 ++cur; // consume backslash, then what follows 554 esc = cur; 555 } 556 ++cur; 557 } 558 if (cur > len) { // 'twas backslash followed by nothing 559 throw new InvalidNameException("Invalid name: " + name); 560 } 561 562 // Trim off (unescaped) trailing whitespace. 563 int end; 564 for (end = cur; end > beg; end--) { 565 if (!isWhitespace(chars[end - 1]) || (esc == end - 1)) { 566 break; 567 } 568 } 569 return new String(chars, beg, end - beg); 570 } 571 572 private void consumeWhitespace() { 573 while ((cur < len) && isWhitespace(chars[cur])) { 574 ++cur; 575 } 576 } 577 578 /* 579 * Returns true if next unconsumed character is one that terminates 580 * a string attribute value. 581 */ 582 private boolean atTerminator() { 583 return (cur < len && 584 (chars[cur] == ',' || 585 chars[cur] == ';' || 586 chars[cur] == '+')); 587 } 588 } 589 590 591 /* 592 * Class Rdn represents a set of TypeAndValue. 593 */ 594 static class Rdn { 595 596 /* 597 * A vector of the TypeAndValue elements of this Rdn. 598 * It is sorted to facilitate set operations. 599 */ 600 private final Vector<TypeAndValue> tvs = new Vector<>(); 601 602 void add(TypeAndValue tv) { 603 604 // Set i to index of first element greater than tv, or to 605 // tvs.size() if there is none. 606 int i; 607 for (i = 0; i < tvs.size(); i++) { 608 int diff = tv.compareTo(tvs.elementAt(i)); 609 if (diff == 0) { 610 return; // tv is a duplicate: ignore it 611 } else if (diff < 0) { 612 break; 613 } 614 } 615 616 tvs.insertElementAt(tv, i); 617 } 618 619 public String toString() { 620 StringBuffer buf = new StringBuffer(); 621 for (int i = 0; i < tvs.size(); i++) { 622 if (i > 0) { 623 buf.append('+'); 624 } 625 buf.append(tvs.elementAt(i)); 626 } 627 return new String(buf); 628 } 629 630 public boolean equals(Object obj) { 631 return ((obj instanceof Rdn) && 632 (compareTo(obj) == 0)); 633 } 634 635 // Compare TypeAndValue components one by one, lexicographically. 636 public int compareTo(Object obj) { 637 Rdn that = (Rdn)obj; 638 int minSize = Math.min(tvs.size(), that.tvs.size()); 639 for (int i = 0; i < minSize; i++) { 640 // Compare a single pair of type/value pairs. 641 TypeAndValue tv = tvs.elementAt(i); 642 int diff = tv.compareTo(that.tvs.elementAt(i)); 643 if (diff != 0) { 644 return diff; 645 } 646 } 647 return (tvs.size() - that.tvs.size()); // longer RDN wins 648 } 649 650 public int hashCode() { 651 // Sum up the hash codes of the components. 652 int hash = 0; 653 654 // For each type/value pair... 655 for (int i = 0; i < tvs.size(); i++) { 656 hash += tvs.elementAt(i).hashCode(); 657 } 658 return hash; 659 } 660 661 Attributes toAttributes() { 662 Attributes attrs = new BasicAttributes(true); 663 TypeAndValue tv; 664 Attribute attr; 665 666 for (int i = 0; i < tvs.size(); i++) { 667 tv = tvs.elementAt(i); 668 if ((attr = attrs.get(tv.getType())) == null) { 669 attrs.put(tv.getType(), tv.getUnescapedValue()); 670 } else { 671 attr.add(tv.getUnescapedValue()); 672 } 673 } 674 return attrs; 675 } 676 } 677 678 679 /* 680 * Class TypeAndValue represents an attribute type and its 681 * corresponding value. 682 */ 683 static class TypeAndValue { 684 685 private final String type; 686 private final String value; // value, escaped or quoted 687 private final boolean binary; 688 private final boolean valueCaseSensitive; 689 690 // If non-null, a canonical representation of the value suitable 691 // for comparison using String.compareTo(). 692 private String comparable = null; 693 694 TypeAndValue(String type, String value, boolean valueCaseSensitive) { 695 this.type = type; 696 this.value = value; 697 binary = value.startsWith("#"); 698 this.valueCaseSensitive = valueCaseSensitive; 699 } 700 701 public String toString() { 702 return (type + "=" + value); 703 } 704 705 public int compareTo(Object obj) { 706 // NB: Any change here affecting equality must be 707 // reflected in hashCode(). 708 709 TypeAndValue that = (TypeAndValue)obj; 710 711 int diff = type.compareToIgnoreCase(that.type); 712 if (diff != 0) { 713 return diff; 714 } 715 if (value.equals(that.value)) { // try shortcut 716 return 0; 717 } 718 return getValueComparable().compareTo(that.getValueComparable()); 719 } 720 721 public boolean equals(Object obj) { 722 // NB: Any change here must be reflected in hashCode(). 723 if (!(obj instanceof TypeAndValue)) { 724 return false; 725 } 726 TypeAndValue that = (TypeAndValue)obj; 727 return (type.equalsIgnoreCase(that.type) && 728 (value.equals(that.value) || 729 getValueComparable().equals(that.getValueComparable()))); 730 } 731 732 public int hashCode() { 733 // If two objects are equal, their hash codes must match. 734 return (type.toUpperCase(Locale.ENGLISH).hashCode() + 735 getValueComparable().hashCode()); 736 } 737 738 /* 739 * Returns the type. 740 */ 741 String getType() { 742 return type; 743 } 744 745 /* 746 * Returns the unescaped value. 747 */ 748 Object getUnescapedValue() { 749 return unescapeValue(value); 750 } 751 752 /* 753 * Returns a canonical representation of "value" suitable for 754 * comparison using String.compareTo(). If "value" is a string, 755 * it is returned with escapes and quotes stripped away, and 756 * hex-encoded UTF-8 converted to 16-bit Unicode chars. 757 * If value's case is to be ignored, it is returned in uppercase. 758 * If "value" is binary, it is returned in uppercase but 759 * otherwise unmodified. 760 */ 761 private String getValueComparable() { 762 if (comparable != null) { 763 return comparable; // return cached result 764 } 765 766 // cache result 767 if (binary) { 768 comparable = value.toUpperCase(Locale.ENGLISH); 769 } else { 770 comparable = (String)unescapeValue(value); 771 if (!valueCaseSensitive) { 772 // ignore case 773 comparable = comparable.toUpperCase(Locale.ENGLISH); 774 } 775 } 776 return comparable; 777 } 778 779 /* 780 * Given the value of an attribute, returns a string suitable 781 * for inclusion in a DN. 782 */ 783 static String escapeValue(Object val) { 784 return (val instanceof byte[]) 785 ? escapeBinaryValue((byte[])val) 786 : escapeStringValue((String)val); 787 } 788 789 /* 790 * Given the value of a string-valued attribute, returns a 791 * string suitable for inclusion in a DN. This is accomplished by 792 * using backslash (\) to escape the following characters: 793 * leading and trailing whitespace 794 * , = + < > # ; " \ 795 */ 796 private static String escapeStringValue(String val) { 797 798 final String escapees = ",=+<>#;\"\\"; 799 char[] chars = val.toCharArray(); 800 StringBuffer buf = new StringBuffer(2 * val.length()); 801 802 // Find leading and trailing whitespace. 803 int lead; // index of first char that is not leading whitespace 804 for (lead = 0; lead < chars.length; lead++) { 805 if (!isWhitespace(chars[lead])) { 806 break; 807 } 808 } 809 int trail; // index of last char that is not trailing whitespace 810 for (trail = chars.length - 1; trail >= 0; trail--) { 811 if (!isWhitespace(chars[trail])) { 812 break; 813 } 814 } 815 816 for (int i = 0; i < chars.length; i++) { 817 char c = chars[i]; 818 if ((i < lead) || (i > trail) || (escapees.indexOf(c) >= 0)) { 819 buf.append('\\'); 820 } 821 buf.append(c); 822 } 823 return new String(buf); 824 } 825 826 /* 827 * Given the value of a binary attribute, returns a string 828 * suitable for inclusion in a DN (such as "#CEB1DF80"). 829 */ 830 private static String escapeBinaryValue(byte[] val) { 831 832 StringBuffer buf = new StringBuffer(1 + 2 * val.length); 833 buf.append("#"); 834 835 for (int i = 0; i < val.length; i++) { 836 byte b = val[i]; 837 buf.append(Character.forDigit(0xF & (b >>> 4), 16)); 838 buf.append(Character.forDigit(0xF & b, 16)); 839 } 840 841 return (new String(buf)).toUpperCase(Locale.ENGLISH); 842 } 843 844 /* 845 * Given an attribute value formatted according to RFC 2253, 846 * returns the unformatted value. Escapes and quotes are 847 * stripped away, and hex-encoded UTF-8 is converted to 16-bit 848 * Unicode chars. Returns a string value as a String, and a 849 * binary value as a byte array. 850 */ 851 static Object unescapeValue(String val) { 852 853 char[] chars = val.toCharArray(); 854 int beg = 0; 855 int end = chars.length; 856 857 // Trim off leading and trailing whitespace. 858 while ((beg < end) && isWhitespace(chars[beg])) { 859 ++beg; 860 } 861 while ((beg < end) && isWhitespace(chars[end - 1])) { 862 --end; 863 } 864 865 // Add back the trailing whitespace with a preceding '\' 866 // (escaped or unescaped) that was taken off in the above 867 // loop. Whether or not to retain this whitespace is 868 // decided below. 869 if (end != chars.length && 870 (beg < end) && 871 chars[end - 1] == '\\') { 872 end++; 873 } 874 if (beg >= end) { 875 return ""; 876 } 877 878 if (chars[beg] == '#') { 879 // Value is binary (eg: "#CEB1DF80"). 880 return decodeHexPairs(chars, ++beg, end); 881 } 882 883 // Trim off quotes. 884 if ((chars[beg] == '\"') && (chars[end - 1] == '\"')) { 885 ++beg; 886 --end; 887 } 888 889 StringBuffer buf = new StringBuffer(end - beg); 890 int esc = -1; // index of the last escaped character 891 892 for (int i = beg; i < end; i++) { 893 if ((chars[i] == '\\') && (i + 1 < end)) { 894 if (!Character.isLetterOrDigit(chars[i + 1])) { 895 ++i; // skip backslash 896 buf.append(chars[i]); // snarf escaped char 897 esc = i; 898 } else { 899 900 // Convert hex-encoded UTF-8 to 16-bit chars. 901 byte[] utf8 = getUtf8Octets(chars, i, end); 902 if (utf8.length > 0) { 903 try { 904 buf.append(new String(utf8, "UTF8")); 905 } catch (java.io.UnsupportedEncodingException e) { 906 // shouldn't happen 907 } 908 i += utf8.length * 3 - 1; 909 } else { 910 throw new IllegalArgumentException( 911 "Not a valid attribute string value:" + 912 val +", improper usage of backslash"); 913 } 914 } 915 } else { 916 buf.append(chars[i]); // snarf unescaped char 917 } 918 } 919 920 // Get rid of the unescaped trailing whitespace with the 921 // preceding '\' character that was previously added back. 922 int len = buf.length(); 923 if (isWhitespace(buf.charAt(len - 1)) && esc != (end - 1)) { 924 buf.setLength(len - 1); 925 } 926 927 return new String(buf); 928 } 929 930 931 /* 932 * Given an array of chars (with starting and ending indexes into it) 933 * representing bytes encoded as hex-pairs (such as "CEB1DF80"), 934 * returns a byte array containing the decoded bytes. 935 */ 936 private static byte[] decodeHexPairs(char[] chars, int beg, int end) { 937 byte[] bytes = new byte[(end - beg) / 2]; 938 for (int i = 0; beg + 1 < end; i++) { 939 int hi = Character.digit(chars[beg], 16); 940 int lo = Character.digit(chars[beg + 1], 16); 941 if (hi < 0 || lo < 0) { 942 break; 943 } 944 bytes[i] = (byte)((hi<<4) + lo); 945 beg += 2; 946 } 947 if (beg != end) { 948 throw new IllegalArgumentException( 949 "Illegal attribute value: #" + new String(chars)); 950 } 951 return bytes; 952 } 953 954 /* 955 * Given an array of chars (with starting and ending indexes into it), 956 * finds the largest prefix consisting of hex-encoded UTF-8 octets, 957 * and returns a byte array containing the corresponding UTF-8 octets. 958 * 959 * Hex-encoded UTF-8 octets look like this: 960 * \03\B1\DF\80 961 */ 962 private static byte[] getUtf8Octets(char[] chars, int beg, int end) { 963 byte[] utf8 = new byte[(end - beg) / 3]; // allow enough room 964 int len = 0; // index of first unused byte in utf8 965 966 while ((beg + 2 < end) && 967 (chars[beg++] == '\\')) { 968 int hi = Character.digit(chars[beg++], 16); 969 int lo = Character.digit(chars[beg++], 16); 970 if (hi < 0 || lo < 0) { 971 break; 972 } 973 utf8[len++] = (byte)((hi<<4) + lo); 974 } 975 976 if (len == utf8.length) { 977 return utf8; 978 } else { 979 byte[] res = new byte[len]; 980 System.arraycopy(utf8, 0, res, 0, len); 981 return res; 982 } 983 } 984 } 985 986 987 /* 988 * For testing. 989 */ 990 /* 991 public static void main(String[] args) { 992 993 try { 994 if (args.length == 1) { // parse and print components 995 LdapName n = new LdapName(args[0]); 996 997 Enumeration rdns = n.rdns.elements(); 998 while (rdns.hasMoreElements()) { 999 Rdn rdn = (Rdn)rdns.nextElement(); 1000 for (int i = 0; i < rdn.tvs.size(); i++) { 1001 System.out.print("[" + rdn.tvs.elementAt(i) + "]"); 1002 } 1003 System.out.println(); 1004 } 1005 1006 } else { // compare two names 1007 LdapName n1 = new LdapName(args[0]); 1008 LdapName n2 = new LdapName(args[1]); 1009 n1.unparsed = null; 1010 n2.unparsed = null; 1011 boolean eq = n1.equals(n2); 1012 System.out.println("[" + n1 + (eq ? "] == [" : "] != [") 1013 + n2 + "]"); 1014 } 1015 } catch (Exception e) { 1016 e.printStackTrace(); 1017 } 1018 } 1019 */ 1020 }