1 /*
   2  * Copyright (c) 2003, 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.List;
  29 import java.util.ArrayList;
  30 
  31 import javax.naming.InvalidNameException;
  32 
  33 /*
  34  * RFC2253Parser implements a recursive descent parser for a single DN.
  35  */
  36 final class Rfc2253Parser {
  37 
  38         private final String name;      // DN being parsed
  39         private final char[] chars;     // characters in LDAP name being parsed
  40         private final int len;  // length of "chars"
  41         private int cur = 0;    // index of first unconsumed char in "chars"
  42 
  43         /*
  44          * Given an LDAP DN in string form, returns a parser for it.
  45          */
  46         Rfc2253Parser(String name) {
  47             this.name = name;
  48             len = name.length();
  49             chars = name.toCharArray();
  50         }
  51 
  52         /*
  53          * Parses the DN, returning a List of its RDNs.
  54          */
  55         // public List<Rdn> getDN() throws InvalidNameException {
  56 
  57         List parseDn() throws InvalidNameException {
  58             cur = 0;
  59 
  60             // ArrayList<Rdn> rdns =
  61             //  new ArrayList<Rdn>(len / 3 + 10);  // leave room for growth
  62 
  63             ArrayList rdns =
  64                 new ArrayList(len / 3 + 10);  // leave room for growth
  65 
  66             if (len == 0) {
  67                 return rdns;
  68             }
  69 
  70             rdns.add(doParse(new Rdn()));
  71             while (cur < len) {
  72                 if (chars[cur] == ',' || chars[cur] == ';') {
  73                     ++cur;
  74                     rdns.add(0, doParse(new Rdn()));
  75                 } else {
  76                     throw new InvalidNameException("Invalid name: " + name);
  77                 }
  78             }
  79             return rdns;
  80         }
  81 
  82         /*
  83          * Parses the DN, if it is known to contain a single RDN.
  84          */
  85         Rdn parseRdn() throws InvalidNameException {
  86             return parseRdn(new Rdn());
  87         }
  88 
  89         /*
  90          * Parses the DN, if it is known to contain a single RDN.
  91          */
  92         Rdn parseRdn(Rdn rdn) throws InvalidNameException {
  93             rdn = doParse(rdn);
  94             if (cur < len) {
  95                 throw new InvalidNameException("Invalid RDN: " + name);
  96             }
  97             return rdn;
  98         }
  99 
 100         /*
 101          * Parses the next RDN and returns it.  Throws an exception if
 102          * none is found.  Leading and trailing whitespace is consumed.
 103          */
 104          private Rdn doParse(Rdn rdn) throws InvalidNameException {
 105 
 106             while (cur < len) {
 107                 consumeWhitespace();
 108                 String attrType = parseAttrType();
 109                 consumeWhitespace();
 110                 if (cur >= len || chars[cur] != '=') {
 111                     throw new InvalidNameException("Invalid name: " + name);
 112                 }
 113                 ++cur;          // consume '='
 114                 consumeWhitespace();
 115                 String value = parseAttrValue();
 116                 consumeWhitespace();
 117 
 118                 rdn.put(attrType, Rdn.unescapeValue(value));
 119                 if (cur >= len || chars[cur] != '+') {
 120                     break;
 121                 }
 122                 ++cur;          // consume '+'
 123             }
 124             rdn.sort();
 125             return rdn;
 126         }
 127 
 128         /*
 129          * Returns the attribute type that begins at the next unconsumed
 130          * char.  No leading whitespace is expected.
 131          * This routine is more generous than RFC 2253.  It accepts
 132          * attribute types composed of any nonempty combination of Unicode
 133          * letters, Unicode digits, '.', '-', and internal space characters.
 134          */
 135         private String parseAttrType() throws InvalidNameException {
 136 
 137             final int beg = cur;
 138             while (cur < len) {
 139                 char c = chars[cur];
 140                 if (Character.isLetterOrDigit(c) ||
 141                         c == '.' ||
 142                         c == '-' ||
 143                         c == ' ') {
 144                     ++cur;
 145                 } else {
 146                     break;
 147                 }
 148             }
 149             // Back out any trailing spaces.
 150             while ((cur > beg) && (chars[cur - 1] == ' ')) {
 151                 --cur;
 152             }
 153 
 154             if (beg == cur) {
 155                 throw new InvalidNameException("Invalid name: " + name);
 156             }
 157             return new String(chars, beg, cur - beg);
 158         }
 159 
 160         /*
 161          * Returns the attribute value that begins at the next unconsumed
 162          * char.  No leading whitespace is expected.
 163          */
 164         private String parseAttrValue() throws InvalidNameException {
 165 
 166             if (cur < len && chars[cur] == '#') {
 167                 return parseBinaryAttrValue();
 168             } else if (cur < len && chars[cur] == '"') {
 169                 return parseQuotedAttrValue();
 170             } else {
 171                 return parseStringAttrValue();
 172             }
 173         }
 174 
 175         private String parseBinaryAttrValue() throws InvalidNameException {
 176             final int beg = cur;
 177             ++cur;                      // consume '#'
 178             while ((cur < len) &&
 179                     Character.isLetterOrDigit(chars[cur])) {
 180                 ++cur;
 181             }
 182             return new String(chars, beg, cur - beg);
 183         }
 184 
 185         private String parseQuotedAttrValue() throws InvalidNameException {
 186 
 187             final int beg = cur;
 188             ++cur;                      // consume '"'
 189 
 190             while ((cur < len) && chars[cur] != '"') {
 191                 if (chars[cur] == '\\') {
 192                     ++cur;              // consume backslash, then what follows
 193                 }
 194                 ++cur;
 195             }
 196             if (cur >= len) {   // no closing quote
 197                 throw new InvalidNameException("Invalid name: " + name);
 198             }
 199             ++cur;      // consume closing quote
 200 
 201             return new String(chars, beg, cur - beg);
 202         }
 203 
 204         private String parseStringAttrValue() throws InvalidNameException {
 205 
 206             final int beg = cur;
 207             int esc = -1;       // index of the most recently escaped character
 208 
 209             while ((cur < len) && !atTerminator()) {
 210                 if (chars[cur] == '\\') {
 211                     ++cur;              // consume backslash, then what follows
 212                     esc = cur;
 213                 }
 214                 ++cur;
 215             }
 216             if (cur > len) {            // 'twas backslash followed by nothing
 217                 throw new InvalidNameException("Invalid name: " + name);
 218             }
 219 
 220             // Trim off (unescaped) trailing whitespace.
 221             int end;
 222             for (end = cur; end > beg; end--) {
 223                 if (!isWhitespace(chars[end - 1]) || (esc == end - 1)) {
 224                     break;
 225                 }
 226             }
 227             return new String(chars, beg, end - beg);
 228         }
 229 
 230         private void consumeWhitespace() {
 231             while ((cur < len) && isWhitespace(chars[cur])) {
 232                 ++cur;
 233             }
 234         }
 235 
 236         /*
 237          * Returns true if next unconsumed character is one that terminates
 238          * a string attribute value.
 239          */
 240         private boolean atTerminator() {
 241             return (cur < len &&
 242                     (chars[cur] == ',' ||
 243                         chars[cur] == ';' ||
 244                         chars[cur] == '+'));
 245         }
 246 
 247         /*
 248          * Best guess as to what RFC 2253 means by "whitespace".
 249          */
 250         private static boolean isWhitespace(char c) {
 251             return (c == ' ' || c == '\r');
 252         }
 253     }