1 /*
   2  * Copyright (c) 2003, 2020, 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 javax.naming.ldap;
  27 
  28 import java.util.Iterator;
  29 import java.util.NoSuchElementException;
  30 import java.util.ArrayList;
  31 import java.util.Locale;
  32 import java.util.Collections;
  33 
  34 import javax.naming.InvalidNameException;
  35 import javax.naming.directory.BasicAttributes;
  36 import javax.naming.directory.Attributes;
  37 import javax.naming.directory.Attribute;
  38 import javax.naming.NamingEnumeration;
  39 import javax.naming.NamingException;
  40 
  41 import java.io.Serializable;
  42 import java.io.ObjectOutputStream;
  43 import java.io.ObjectInputStream;
  44 import java.io.IOException;
  45 
  46 /**
  47  * This class represents a relative distinguished name, or RDN, which is a
  48  * component of a distinguished name as specified by
  49  * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>.
  50  * An example of an RDN is "OU=Sales+CN=J.Smith". In this example,
  51  * the RDN consist of multiple attribute type/value pairs. The
  52  * RDN is parsed as described in the class description for
  53  * {@link javax.naming.ldap.LdapName LdapName}.
  54  * <p>
  55  * The Rdn class represents an RDN as attribute type/value mappings,
  56  * which can be viewed using
  57  * {@link javax.naming.directory.Attributes Attributes}.
  58  * In addition, it contains convenience methods that allow easy retrieval
  59  * of type and value when the Rdn consist of a single type/value pair,
  60  * which is how it appears in a typical usage.
  61  * It also contains helper methods that allow escaping of the unformatted
  62  * attribute value and unescaping of the value formatted according to the
  63  * escaping syntax defined in RFC2253. For methods that take or return
  64  * attribute value as an Object, the value is either a String
  65  * (in unescaped form) or a byte array.
  66  * <p>
  67  * <code>Rdn</code> will properly parse all valid RDNs, but
  68  * does not attempt to detect all possible violations when parsing
  69  * invalid RDNs. It is "generous" in accepting invalid RDNs.
  70  * The "validity" of a name is determined ultimately when it
  71  * is supplied to an LDAP server, which may accept or
  72  * reject the name based on factors such as its schema information
  73  * and interoperability considerations.
  74  *
  75  * <p>
  76  * The following code example shows how to construct an Rdn using the
  77  * constructor that takes type and value as arguments:
  78  * <pre>
  79  *      Rdn rdn = new Rdn("cn", "Juicy, Fruit");
  80  *      System.out.println(rdn.toString());
  81  * </pre>
  82  * The last line will print {@code cn=Juicy\, Fruit}. The
  83  * {@link #unescapeValue(String) unescapeValue()} method can be
  84  * used to unescape the escaped comma resulting in the original
  85  * value {@code "Juicy, Fruit"}. The {@link #escapeValue(Object)
  86  * escapeValue()} method adds the escape back preceding the comma.
  87  * <p>
  88  * This class can be instantiated by a string representation
  89  * of the RDN defined in RFC 2253 as shown in the following code example:
  90  * <pre>
  91  *      Rdn rdn = new Rdn("cn=Juicy\\, Fruit");
  92  *      System.out.println(rdn.toString());
  93  * </pre>
  94  * The last line will print {@code cn=Juicy\, Fruit}.
  95  * <p>
  96  * Concurrent multithreaded read-only access of an instance of
  97  * {@code Rdn} need not be synchronized.
  98  * <p>
  99  * Unless otherwise noted, the behavior of passing a null argument
 100  * to a constructor or method in this class will cause NullPointerException
 101  * to be thrown.
 102  *
 103  * @since 1.5
 104  */
 105 
 106 public class Rdn implements Serializable, Comparable<Object> {
 107 
 108     private transient ArrayList<RdnEntry> entries;
 109 
 110     // The common case.
 111     private static final int DEFAULT_SIZE = 1;
 112 
 113     private static final long serialVersionUID = -5994465067210009656L;
 114 
 115     /**
 116      * Constructs an Rdn from the given attribute set. See
 117      * {@link javax.naming.directory.Attributes Attributes}.
 118      * <p>
 119      * The string attribute values are not interpreted as
 120      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
 121      * formatted RDN strings. That is, the values are used
 122      * literally (not parsed) and assumed to be unescaped.
 123      *
 124      * @param attrSet The non-null and non-empty attributes containing
 125      * type/value mappings.
 126      * @throws InvalidNameException If contents of {@code attrSet} cannot
 127      *          be used to construct a valid RDN.
 128      */
 129     public Rdn(Attributes attrSet) throws InvalidNameException {
 130         if (attrSet.size() == 0) {
 131             throw new InvalidNameException("Attributes cannot be empty");
 132         }
 133         entries = new ArrayList<>(attrSet.size());
 134         NamingEnumeration<? extends Attribute> attrs = attrSet.getAll();
 135         try {
 136             for (int nEntries = 0; attrs.hasMore(); nEntries++) {
 137                 RdnEntry entry = new RdnEntry();
 138                 Attribute attr = attrs.next();
 139                 entry.type = attr.getID();
 140                 entry.value = attr.get();
 141                 entries.add(nEntries, entry);
 142             }
 143         } catch (NamingException e) {
 144             InvalidNameException e2 = new InvalidNameException(
 145                                         e.getMessage());
 146             e2.initCause(e);
 147             throw e2;
 148         }
 149         sort(); // arrange entries for comparison
 150     }
 151 
 152     /**
 153      * Constructs an Rdn from the given string.
 154      * This constructor takes a string formatted according to the rules
 155      * defined in <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
 156      * and described in the class description for
 157      * {@link javax.naming.ldap.LdapName}.
 158      *
 159      * @param rdnString The non-null and non-empty RFC2253 formatted string.
 160      * @throws InvalidNameException If a syntax error occurs during
 161      *                  parsing of the rdnString.
 162      */
 163     public Rdn(String rdnString) throws InvalidNameException {
 164         entries = new ArrayList<>(DEFAULT_SIZE);
 165         (new Rfc2253Parser(rdnString)).parseRdn(this);
 166     }
 167 
 168     /**
 169      * Constructs an Rdn from the given {@code rdn}.
 170      * The contents of the {@code rdn} are simply copied into the newly
 171      * created Rdn.
 172      * @param rdn The non-null Rdn to be copied.
 173      */
 174     public Rdn(Rdn rdn) {
 175         entries = new ArrayList<>(rdn.entries.size());
 176         entries.addAll(rdn.entries);
 177     }
 178 
 179     /**
 180      * Constructs an Rdn from the given attribute type and
 181      * value.
 182      * The string attribute values are not interpreted as
 183      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
 184      * formatted RDN strings. That is, the values are used
 185      * literally (not parsed) and assumed to be unescaped.
 186      *
 187      * @param type The non-null and non-empty string attribute type.
 188      * @param value The non-null and non-empty attribute value.
 189      * @throws InvalidNameException If type/value cannot be used to
 190      *                  construct a valid RDN.
 191      * @see #toString()
 192      */
 193     public Rdn(String type, Object value) throws InvalidNameException {
 194         if (value == null) {
 195             throw new NullPointerException("Cannot set value to null");
 196         }
 197         if (type.equals("") || isEmptyValue(value)) {
 198             throw new InvalidNameException(
 199                 "type or value cannot be empty, type:" + type +
 200                 " value:" + value);
 201         }
 202         entries = new ArrayList<>(DEFAULT_SIZE);
 203         put(type, value);
 204     }
 205 
 206     private boolean isEmptyValue(Object val) {
 207         return ((val instanceof String) && val.equals("")) ||
 208         ((val instanceof byte[]) && (((byte[]) val).length == 0));
 209     }
 210 
 211     // An empty constructor used by the parser
 212     Rdn() {
 213         entries = new ArrayList<>(DEFAULT_SIZE);
 214     }
 215 
 216     /*
 217      * Adds the given attribute type and value to this Rdn.
 218      * The string attribute values are not interpreted as
 219      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>
 220      * formatted RDN strings. That is the values are used
 221      * literally (not parsed) and assumed to be unescaped.
 222      *
 223      * @param type The non-null and non-empty string attribute type.
 224      * @param value The non-null and non-empty attribute value.
 225      * @return The updated Rdn, not a new one. Cannot be null.
 226      * @see #toString()
 227      */
 228     Rdn put(String type, Object value) {
 229 
 230         // create new Entry
 231         RdnEntry newEntry = new RdnEntry();
 232         newEntry.type =  type;
 233         if (value instanceof byte[]) {  // clone the byte array
 234             newEntry.value = ((byte[]) value).clone();
 235         } else {
 236             newEntry.value = value;
 237         }
 238         entries.add(newEntry);
 239         return this;
 240     }
 241 
 242     void sort() {
 243         if (entries.size() > 1) {
 244             Collections.sort(entries);
 245         }
 246     }
 247 
 248     /**
 249      * Retrieves one of this Rdn's value.
 250      * This is a convenience method for obtaining the value,
 251      * when the RDN contains a single type and value mapping,
 252      * which is the common RDN usage.
 253      * <p>
 254      * For a multi-valued RDN, this method returns value corresponding
 255      * to the type returned by {@link #getType() getType()} method.
 256      *
 257      * @return The non-null attribute value.
 258      */
 259     public Object getValue() {
 260         return entries.get(0).getValue();
 261     }
 262 
 263     /**
 264      * Retrieves one of this Rdn's type.
 265      * This is a convenience method for obtaining the type,
 266      * when the RDN contains a single type and value mapping,
 267      * which is the common RDN usage.
 268      * <p>
 269      * For a multi-valued RDN, the type/value pairs have
 270      * no specific order defined on them. In that case, this method
 271      * returns type of one of the type/value pairs.
 272      * The {@link #getValue() getValue()} method returns the
 273      * value corresponding to the type returned by this method.
 274      *
 275      * @return The non-null attribute type.
 276      */
 277     public String getType() {
 278         return entries.get(0).getType();
 279     }
 280 
 281     /**
 282      * Returns this Rdn as a string represented in a format defined by
 283      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a> and described
 284      * in the class description for {@link javax.naming.ldap.LdapName LdapName}.
 285      *
 286      * @return The string representation of the Rdn.
 287      */
 288     public String toString() {
 289         StringBuilder builder = new StringBuilder();
 290         int size = entries.size();
 291         if (size > 0) {
 292             builder.append(entries.get(0));
 293         }
 294         for (int next = 1; next < size; next++) {
 295             builder.append('+');
 296             builder.append(entries.get(next));
 297         }
 298         return builder.toString();
 299     }
 300 
 301     /**
 302      * Compares this Rdn with the specified Object for order.
 303      * Returns a negative integer, zero, or a positive integer as this
 304      * Rdn is less than, equal to, or greater than the given Object.
 305      * <p>
 306      * If obj is null or not an instance of Rdn, ClassCastException
 307      * is thrown.
 308      * <p>
 309      * The attribute type and value pairs of the RDNs are lined up
 310      * against each other and compared lexicographically. The order of
 311      * components in multi-valued Rdns (such as "ou=Sales+cn=Bob") is not
 312      * significant.
 313      *
 314      * @param obj The non-null object to compare against.
 315      * @return  A negative integer, zero, or a positive integer as this Rdn
 316      *          is less than, equal to, or greater than the given Object.
 317      * @exception ClassCastException if obj is null or not a Rdn.
 318      */
 319     public int compareTo(Object obj) {
 320         if (!(obj instanceof Rdn)) {
 321             throw new ClassCastException("The obj is not a Rdn");
 322         }
 323         if (obj == this) {
 324             return 0;
 325         }
 326         Rdn that = (Rdn) obj;
 327         int minSize = Math.min(entries.size(), that.entries.size());
 328         for (int i = 0; i < minSize; i++) {
 329 
 330             // Compare a single pair of type/value pairs.
 331             int diff = entries.get(i).compareTo(that.entries.get(i));
 332             if (diff != 0) {
 333                 return diff;
 334             }
 335         }
 336         return (entries.size() - that.entries.size());  // longer RDN wins
 337     }
 338 
 339     /**
 340      * Compares the specified Object with this Rdn for equality.
 341      * Returns true if the given object is also a Rdn and the two Rdns
 342      * represent the same attribute type and value mappings. The order of
 343      * components in multi-valued Rdns (such as "ou=Sales+cn=Bob") is not
 344      * significant.
 345      * <p>
 346      * Type and value equality matching is done as below:
 347      * <ul>
 348      * <li> The types are compared for equality with their case ignored.
 349      * <li> String values with different but equivalent usage of quoting,
 350      * escaping, or UTF8-hex-encoding are considered equal.
 351      * The case of the values is ignored during the comparison.
 352      * </ul>
 353      * <p>
 354      * If obj is null or not an instance of Rdn, false is returned.
 355      *
 356      * @param obj object to be compared for equality with this Rdn.
 357      * @return true if the specified object is equal to this Rdn.
 358      * @see #hashCode()
 359      */
 360     public boolean equals(Object obj) {
 361         if (obj == this) {
 362             return true;
 363         }
 364         if (!(obj instanceof Rdn)) {
 365             return false;
 366         }
 367         Rdn that = (Rdn) obj;
 368         if (entries.size() != that.size()) {
 369             return false;
 370         }
 371         for (int i = 0; i < entries.size(); i++) {
 372             if (!entries.get(i).equals(that.entries.get(i))) {
 373                 return false;
 374             }
 375         }
 376         return true;
 377     }
 378 
 379     /**
 380      * Returns the hash code of this RDN. Two RDNs that are
 381      * equal (according to the equals method) will have the same
 382      * hash code.
 383      *
 384      * @return An int representing the hash code of this Rdn.
 385      * @see #equals
 386      */
 387     public int hashCode() {
 388 
 389         // Sum up the hash codes of the components.
 390         int hash = 0;
 391 
 392         // For each type/value pair...
 393         for (int i = 0; i < entries.size(); i++) {
 394             hash += entries.get(i).hashCode();
 395         }
 396         return hash;
 397     }
 398 
 399     /**
 400      * Retrieves the {@link javax.naming.directory.Attributes Attributes}
 401      * view of the type/value mappings contained in this Rdn.
 402      *
 403      * @return  The non-null attributes containing the type/value
 404      *          mappings of this Rdn.
 405      */
 406     public Attributes toAttributes() {
 407         Attributes attrs = new BasicAttributes(true);
 408         for (int i = 0; i < entries.size(); i++) {
 409             RdnEntry entry = entries.get(i);
 410             Attribute attr = attrs.put(entry.getType(), entry.getValue());
 411             if (attr != null) {
 412                 attr.add(entry.getValue());
 413                 attrs.put(attr);
 414             }
 415         }
 416         return attrs;
 417     }
 418 
 419 
 420     private static class RdnEntry implements Comparable<RdnEntry> {
 421         private String type;
 422         private Object value;
 423 
 424         // If non-null, a canonical representation of the value suitable
 425         // for comparison using String.compareTo()
 426         private String comparable = null;
 427 
 428         String getType() {
 429             return type;
 430         }
 431 
 432         Object getValue() {
 433             return value;
 434         }
 435 
 436         public int compareTo(RdnEntry that) {
 437             int diff = type.compareToIgnoreCase(that.type);
 438             if (diff != 0) {
 439                 return diff;
 440             }
 441             if (value.equals(that.value)) {     // try shortcut
 442                 return 0;
 443             }
 444             return getValueComparable().compareTo(
 445                         that.getValueComparable());
 446         }
 447 
 448         public boolean equals(Object obj) {
 449             if (obj == this) {
 450                 return true;
 451             }
 452             if (!(obj instanceof RdnEntry)) {
 453                 return false;
 454             }
 455 
 456             // Any change here must be reflected in hashCode()
 457             RdnEntry that = (RdnEntry) obj;
 458             return (type.equalsIgnoreCase(that.type)) &&
 459                         (getValueComparable().equals(
 460                         that.getValueComparable()));
 461         }
 462 
 463         public int hashCode() {
 464             return (type.toUpperCase(Locale.ENGLISH).hashCode() +
 465                 getValueComparable().hashCode());
 466         }
 467 
 468         public String toString() {
 469             return type + "=" + escapeValue(value);
 470         }
 471 
 472         private String getValueComparable() {
 473             if (comparable != null) {
 474                 return comparable;              // return cached result
 475             }
 476 
 477             // cache result
 478             if (value instanceof byte[]) {
 479                 comparable = escapeBinaryValue((byte[]) value);
 480             } else {
 481                 comparable = ((String) value).toUpperCase(Locale.ENGLISH);
 482             }
 483             return comparable;
 484         }
 485     }
 486 
 487     /**
 488      * Retrieves the number of attribute type/value pairs in this Rdn.
 489      * @return The non-negative number of type/value pairs in this Rdn.
 490      */
 491     public int size() {
 492         return entries.size();
 493     }
 494 
 495     /**
 496      * Given the value of an attribute, returns a string escaped according
 497      * to the rules specified in
 498      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>.
 499      * <p>
 500      * For example, if the val is "Sue, Grabbit and Runn", the escaped
 501      * value returned by this method is "Sue\, Grabbit and Runn".
 502      * <p>
 503      * A string value is represented as a String and binary value
 504      * as a byte array.
 505      *
 506      * @param val The non-null object to be escaped.
 507      * @return Escaped string value.
 508      * @throws ClassCastException if val is not a String or byte array.
 509      */
 510     public static String escapeValue(Object val) {
 511         return (val instanceof byte[])
 512                 ? escapeBinaryValue((byte[])val)
 513                 : escapeStringValue((String)val);
 514     }
 515 
 516     /*
 517      * Given the value of a string-valued attribute, returns a
 518      * string suitable for inclusion in a DN.  This is accomplished by
 519      * using backslash (\) to escape the following characters:
 520      *  leading and trailing whitespace
 521      *  , = + < > # ; " \
 522      */
 523     private static final String escapees = ",=+<>#;\"\\";
 524 
 525     private static String escapeStringValue(String val) {
 526 
 527             char[] chars = val.toCharArray();
 528             StringBuilder builder = new StringBuilder(2 * val.length());
 529 
 530             // Find leading and trailing whitespace.
 531             int lead;   // index of first char that is not leading whitespace
 532             for (lead = 0; lead < chars.length; lead++) {
 533                 if (!isWhitespace(chars[lead])) {
 534                     break;
 535                 }
 536             }
 537             int trail;  // index of last char that is not trailing whitespace
 538             for (trail = chars.length - 1; trail >= 0; trail--) {
 539                 if (!isWhitespace(chars[trail])) {
 540                     break;
 541                 }
 542             }
 543 
 544             for (int i = 0; i < chars.length; i++) {
 545                 char c = chars[i];
 546                 if ((i < lead) || (i > trail) || (escapees.indexOf(c) >= 0)) {
 547                     builder.append('\\');
 548                 }
 549                 builder.append(c);
 550             }
 551             return builder.toString();
 552     }
 553 
 554     /*
 555      * Given the value of a binary attribute, returns a string
 556      * suitable for inclusion in a DN (such as "#CEB1DF80").
 557      * TBD: This method should actually generate the ber encoding
 558      * of the binary value
 559      */
 560     private static String escapeBinaryValue(byte[] val) {
 561 
 562         StringBuilder builder = new StringBuilder(1 + 2 * val.length);
 563         builder.append("#");
 564 
 565         for (int i = 0; i < val.length; i++) {
 566             byte b = val[i];
 567             builder.append(Character.forDigit(0xF & (b >>> 4), 16));
 568             builder.append(Character.forDigit(0xF & b, 16));
 569         }
 570         return builder.toString();
 571     }
 572 
 573     /**
 574      * Given an attribute value string formatted according to the rules
 575      * specified in
 576      * <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>,
 577      * returns the unformatted value.  Escapes and quotes are
 578      * stripped away, and hex-encoded UTF-8 is converted to equivalent
 579      * UTF-16 characters. Returns a string value as a String, and a
 580      * binary value as a byte array.
 581      * <p>
 582      * Legal and illegal values are defined in RFC 2253.
 583      * This method is generous in accepting the values and does not
 584      * catch all illegal values.
 585      * Therefore, passing in an illegal value might not necessarily
 586      * trigger an {@code IllegalArgumentException}.
 587      *
 588      * @param   val     The non-null string to be unescaped.
 589      * @return          Unescaped value.
 590      * @throws          IllegalArgumentException When an Illegal value
 591      *                  is provided.
 592      */
 593     public static Object unescapeValue(String val) {
 594 
 595             char[] chars = val.toCharArray();
 596             int beg = 0;
 597             int end = chars.length;
 598 
 599             // Trim off leading and trailing whitespace.
 600             while ((beg < end) && isWhitespace(chars[beg])) {
 601                 ++beg;
 602             }
 603 
 604             while ((beg < end) && isWhitespace(chars[end - 1])) {
 605                 --end;
 606             }
 607 
 608             // Add back the trailing whitespace with a preceding '\'
 609             // (escaped or unescaped) that was taken off in the above
 610             // loop. Whether or not to retain this whitespace is decided below.
 611             if (end != chars.length &&
 612                     (beg < end) &&
 613                     chars[end - 1] == '\\') {
 614                 end++;
 615             }
 616             if (beg >= end) {
 617                 return "";
 618             }
 619 
 620             if (chars[beg] == '#') {
 621                 // Value is binary (eg: "#CEB1DF80").
 622                 return decodeHexPairs(chars, ++beg, end);
 623             }
 624 
 625             // Trim off quotes.
 626             if ((chars[beg] == '\"') && (chars[end - 1] == '\"')) {
 627                 ++beg;
 628                 --end;
 629             }
 630 
 631             StringBuilder builder = new StringBuilder(end - beg);
 632             int esc = -1; // index of the last escaped character
 633 
 634             for (int i = beg; i < end; i++) {
 635                 if ((chars[i] == '\\') && (i + 1 < end)) {
 636                     if (!Character.isLetterOrDigit(chars[i + 1])) {
 637                         ++i;                            // skip backslash
 638                         builder.append(chars[i]);       // snarf escaped char
 639                         esc = i;
 640                     } else {
 641 
 642                         // Convert hex-encoded UTF-8 to 16-bit chars.
 643                         byte[] utf8 = getUtf8Octets(chars, i, end);
 644                         if (utf8.length > 0) {
 645                             try {
 646                                 builder.append(new String(utf8, "UTF8"));
 647                             } catch (java.io.UnsupportedEncodingException e) {
 648                                 // shouldn't happen
 649                             }
 650                             i += utf8.length * 3 - 1;
 651                         } else { // no utf8 bytes available, invalid DN
 652 
 653                             // '/' has no meaning, throw exception
 654                             throw new IllegalArgumentException(
 655                                 "Not a valid attribute string value:" +
 656                                 val + ",improper usage of backslash");
 657                         }
 658                     }
 659                 } else {
 660                     builder.append(chars[i]);   // snarf unescaped char
 661                 }
 662             }
 663 
 664             // Get rid of the unescaped trailing whitespace with the
 665             // preceding '\' character that was previously added back.
 666             int len = builder.length();
 667             if (isWhitespace(builder.charAt(len - 1)) && esc != (end - 1)) {
 668                 builder.setLength(len - 1);
 669             }
 670             return builder.toString();
 671         }
 672 
 673 
 674         /*
 675          * Given an array of chars (with starting and ending indexes into it)
 676          * representing bytes encoded as hex-pairs (such as "CEB1DF80"),
 677          * returns a byte array containing the decoded bytes.
 678          */
 679         private static byte[] decodeHexPairs(char[] chars, int beg, int end) {
 680             byte[] bytes = new byte[(end - beg) / 2];
 681             for (int i = 0; beg + 1 < end; i++) {
 682                 int hi = Character.digit(chars[beg], 16);
 683                 int lo = Character.digit(chars[beg + 1], 16);
 684                 if (hi < 0 || lo < 0) {
 685                     break;
 686                 }
 687                 bytes[i] = (byte)((hi<<4) + lo);
 688                 beg += 2;
 689             }
 690             if (beg != end) {
 691                 throw new IllegalArgumentException(
 692                         "Illegal attribute value: " + new String(chars));
 693             }
 694             return bytes;
 695         }
 696 
 697         /*
 698          * Given an array of chars (with starting and ending indexes into it),
 699          * finds the largest prefix consisting of hex-encoded UTF-8 octets,
 700          * and returns a byte array containing the corresponding UTF-8 octets.
 701          *
 702          * Hex-encoded UTF-8 octets look like this:
 703          *      \03\B1\DF\80
 704          */
 705         private static byte[] getUtf8Octets(char[] chars, int beg, int end) {
 706             byte[] utf8 = new byte[(end - beg) / 3];    // allow enough room
 707             int len = 0;        // index of first unused byte in utf8
 708 
 709             while ((beg + 2 < end) &&
 710                    (chars[beg++] == '\\')) {
 711                 int hi = Character.digit(chars[beg++], 16);
 712                 int lo = Character.digit(chars[beg++], 16);
 713                 if (hi < 0 || lo < 0) {
 714                    break;
 715                 }
 716                 utf8[len++] = (byte)((hi<<4) + lo);
 717             }
 718             if (len == utf8.length) {
 719                 return utf8;
 720             } else {
 721                 byte[] res = new byte[len];
 722                 System.arraycopy(utf8, 0, res, 0, len);
 723                 return res;
 724             }
 725         }
 726 
 727     /*
 728      * Best guess as to what RFC 2253 means by "whitespace".
 729      */
 730     private static boolean isWhitespace(char c) {
 731         return (c == ' ' || c == '\r');
 732     }
 733 
 734     /**
 735      * Serializes only the unparsed RDN, for compactness and to avoid
 736      * any implementation dependency.
 737      *
 738      * @serialData The unparsed RDN {@code String} representation.
 739      *
 740      * @param s the {@code ObjectOutputStream} to write to
 741      * @throws java.io.IOException if an I/O error occurs.
 742      */
 743     @java.io.Serial
 744     private void writeObject(ObjectOutputStream s)
 745             throws java.io.IOException {
 746         s.defaultWriteObject();
 747         s.writeObject(toString());
 748     }
 749 
 750     /**
 751      * Initializes the {@code Rdn} from deserialized data.
 752      *
 753      * See {@code writeObject} for a description of the serial form.
 754      *
 755      * @param s the {@code ObjectInputStream} to read from
 756      * @throws IOException if an I/O error occurs.
 757      * @throws ClassNotFoundException if the class of a serialized object
 758      *         could not be found.
 759      */
 760     @java.io.Serial
 761     private void readObject(ObjectInputStream s)
 762             throws IOException, ClassNotFoundException {
 763         s.defaultReadObject();
 764         entries = new ArrayList<>(DEFAULT_SIZE);
 765         String unparsed = (String) s.readObject();
 766         try {
 767             (new Rfc2253Parser(unparsed)).parseRdn(this);
 768         } catch (InvalidNameException e) {
 769             // shouldn't happen
 770             throw new java.io.StreamCorruptedException(
 771                     "Invalid name: " + unparsed);
 772         }
 773     }
 774 }