001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.server.authorization.dseecompat;
028
029import static org.opends.messages.AccessControlMessages.*;
030import static org.opends.messages.SchemaMessages.*;
031import static org.opends.server.util.CollectionUtils.*;
032import static org.opends.server.util.StaticUtils.*;
033
034import java.util.ArrayList;
035import java.util.List;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.i18n.slf4j.LocalizedLogger;
039import org.forgerock.opendj.ldap.ByteString;
040import org.forgerock.opendj.ldap.ResultCode;
041import org.forgerock.util.Reject;
042import org.opends.server.types.DN;
043import org.opends.server.types.DirectoryException;
044
045/**
046 * This class is used to encapsulate DN pattern matching using wildcards.
047 * The following wildcard uses are supported.
048 *
049 * Value substring:  Any number of wildcards may appear in RDN attribute
050 * values where they match zero or more characters, just like substring filters:
051 *   uid=b*jensen*
052 *
053 * Whole-Type:  A single wildcard may also be used to match any RDN attribute
054 * type, and the wildcard in this case may be omitted as a shorthand:
055 *   *=bjensen
056 *   bjensen
057 *
058 * Whole-RDN.  A single wildcard may be used to match exactly one RDN component
059 * (which may be single or multi-valued):
060 *   *,dc=example,dc=com
061 *
062 * Multiple-Whole-RDN:  A double wildcard may be used to match one or more
063 * RDN components:
064 *   uid=bjensen,**,dc=example,dc=com
065 *
066 */
067public class PatternDN
068{
069  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
070
071  /**
072   * If the pattern did not include any Multiple-Whole-RDN wildcards, then
073   * this is the sequence of RDN patterns in the DN pattern.  Otherwise it
074   * is null.
075   */
076  PatternRDN[] equality;
077
078
079  /**
080   * If the pattern included any Multiple-Whole-RDN wildcards, then these
081   * are the RDN pattern sequences that appear between those wildcards.
082   */
083  PatternRDN[] subInitial;
084  List<PatternRDN[]> subAnyElements;
085  PatternRDN[] subFinal;
086
087
088  /**
089   * When there is no initial sequence, this is used to distinguish between
090   * the case where we have a suffix pattern (zero or more RDN components
091   * allowed before matching elements) and the case where it is not a
092   * suffix pattern but the pattern started with a Multiple-Whole-RDN wildcard
093   * (one or more RDN components allowed before matching elements).
094   */
095  boolean isSuffix;
096
097
098  /**
099   * Create a DN pattern that does not include any Multiple-Whole-RDN wildcards.
100   * @param equality The sequence of RDN patterns making up the DN pattern.
101   */
102  private PatternDN(PatternRDN[] equality)
103  {
104    this.equality = equality;
105  }
106
107
108  /**
109   * Create a DN pattern that includes Multiple-Whole-RDN wildcards.
110   * @param subInitial     The sequence of RDN patterns appearing at the
111   *                       start of the DN, or null if there are none.
112   * @param subAnyElements The list of sequences of RDN patterns appearing
113   *                       in order anywhere in the DN.
114   * @param subFinal       The sequence of RDN patterns appearing at the
115   *                       end of the DN, or null if there are none.
116   */
117  private PatternDN(PatternRDN[] subInitial,
118                    List<PatternRDN[]> subAnyElements,
119                    PatternRDN[] subFinal)
120  {
121    Reject.ifNull(subAnyElements);
122    this.subInitial = subInitial;
123    this.subAnyElements = subAnyElements;
124    this.subFinal = subFinal;
125  }
126
127
128  /**
129   * Determine whether a given DN matches this pattern.
130   * @param dn The DN to be matched.
131   * @return true if the DN matches the pattern.
132   */
133  public boolean matchesDN(DN dn)
134  {
135    if (equality != null)
136    {
137      // There are no Multiple-Whole-RDN wildcards in the pattern.
138      if (equality.length != dn.size())
139      {
140        return false;
141      }
142
143      for (int i = 0; i < dn.size(); i++)
144      {
145        if (!equality[i].matchesRDN(dn.getRDN(i)))
146        {
147          return false;
148        }
149      }
150
151      return true;
152    }
153    else
154    {
155      // There are Multiple-Whole-RDN wildcards in the pattern.
156      int valueLength = dn.size();
157
158      int pos = 0;
159      if (subInitial != null)
160      {
161        int initialLength = subInitial.length;
162        if (initialLength > valueLength)
163        {
164          return false;
165        }
166
167        for (; pos < initialLength; pos++)
168        {
169          if (!subInitial[pos].matchesRDN(dn.getRDN(pos)))
170          {
171            return false;
172          }
173        }
174        pos++;
175      }
176      else
177      {
178        if (!isSuffix)
179        {
180          pos++;
181        }
182      }
183
184
185      if (subAnyElements != null && ! subAnyElements.isEmpty())
186      {
187        for (PatternRDN[] element : subAnyElements)
188        {
189          int anyLength = element.length;
190
191          int end = valueLength - anyLength;
192          boolean match = false;
193          for (; pos < end; pos++)
194          {
195            if (element[0].matchesRDN(dn.getRDN(pos)))
196            {
197              if (subMatch(dn, pos, element, anyLength))
198              {
199                match = true;
200                break;
201              }
202            }
203          }
204
205          if (match)
206          {
207            pos += anyLength + 1;
208          }
209          else
210          {
211            return false;
212          }
213        }
214      }
215
216
217      if (subFinal != null)
218      {
219        int finalLength = subFinal.length;
220
221        if (valueLength - finalLength < pos)
222        {
223          return false;
224        }
225
226        pos = valueLength - finalLength;
227        for (int i=0; i < finalLength; i++,pos++)
228        {
229          if (!subFinal[i].matchesRDN(dn.getRDN(pos)))
230          {
231            return false;
232          }
233        }
234      }
235
236      return pos <= valueLength;
237    }
238  }
239
240  private boolean subMatch(DN dn, int pos, PatternRDN[] element, int length)
241  {
242    for (int i = 1; i < length; i++)
243    {
244      if (!element[i].matchesRDN(dn.getRDN(pos + i)))
245      {
246        return false;
247      }
248    }
249    return true;
250  }
251
252  /**
253   * Create a new DN pattern matcher to match a suffix.
254   * @param pattern The suffix pattern string.
255   * @throws org.opends.server.types.DirectoryException If the pattern string
256   * is not valid.
257   * @return A new DN pattern matcher.
258   */
259  public static PatternDN decodeSuffix(String pattern)
260       throws DirectoryException
261  {
262    // Parse the user supplied pattern.
263    PatternDN patternDN = decode(pattern);
264
265    // Adjust the pattern so that it matches any DN ending with the pattern.
266    if (patternDN.equality != null)
267    {
268      // The pattern contained no Multiple-Whole-RDN wildcards,
269      // so we just convert the whole thing into a final fragment.
270      patternDN.subInitial = null;
271      patternDN.subFinal = patternDN.equality;
272      patternDN.subAnyElements = null;
273      patternDN.equality = null;
274    }
275    else if (patternDN.subInitial != null)
276    {
277      // The pattern had an initial fragment so we need to convert that into
278      // the head of the list of any elements.
279      patternDN.subAnyElements.add(0, patternDN.subInitial);
280      patternDN.subInitial = null;
281    }
282    patternDN.isSuffix = true;
283    return patternDN;
284  }
285
286
287  /**
288   * Create a new DN pattern matcher from a pattern string.
289   * @param dnString The DN pattern string.
290   * @throws org.opends.server.types.DirectoryException If the pattern string
291   * is not valid.
292   * @return A new DN pattern matcher.
293   */
294  public static PatternDN decode(String dnString)
295         throws DirectoryException
296  {
297    ArrayList<PatternRDN> rdnComponents = new ArrayList<>();
298    ArrayList<Integer> doubleWildPos = new ArrayList<>();
299
300    // A null or empty DN is acceptable.
301    if (dnString == null)
302    {
303      return new PatternDN(new PatternRDN[0]);
304    }
305
306    int length = dnString.length();
307    if (length == 0)
308    {
309      return new PatternDN(new PatternRDN[0]);
310    }
311
312
313    // Iterate through the DN string.  The first thing to do is to get
314    // rid of any leading spaces.
315    int pos = 0;
316    char c = dnString.charAt(pos);
317    while (c == ' ')
318    {
319      pos++;
320      if (pos == length)
321      {
322        // This means that the DN was completely comprised of spaces
323        // and therefore should be considered the same as a null or empty DN.
324        return new PatternDN(new PatternRDN[0]);
325      }
326      else
327      {
328        c = dnString.charAt(pos);
329      }
330    }
331
332    // We know that it's not an empty DN, so we can do the real
333    // processing. Create a loop and iterate through all the RDN components.
334    rdnLoop:
335    while (true)
336    {
337      int attributePos = pos;
338      StringBuilder attributeName = new StringBuilder();
339      pos = parseAttributePattern(dnString, pos, attributeName);
340      String name            = attributeName.toString();
341
342
343      // Make sure that we're not at the end of the DN string because
344      // that would be invalid.
345      if (pos >= length)
346      {
347        if (name.equals("*"))
348        {
349          rdnComponents.add(new PatternRDN(name, null, dnString));
350          break;
351        }
352        else if (name.equals("**"))
353        {
354          doubleWildPos.add(rdnComponents.size());
355          break;
356        }
357        else
358        {
359          pos = attributePos - 1;
360          name = "*";
361          c = '=';
362        }
363      }
364      else
365      {
366        // Skip over any spaces between the attribute name and its
367        // value.
368        c = dnString.charAt(pos);
369        while (c == ' ')
370        {
371          pos++;
372          if (pos >= length)
373          {
374            if (name.equals("*"))
375            {
376              rdnComponents.add(new PatternRDN(name, null, dnString));
377              break rdnLoop;
378            }
379            else if (name.equals("**"))
380            {
381              doubleWildPos.add(rdnComponents.size());
382              break rdnLoop;
383            }
384            else
385            {
386              pos = attributePos - 1;
387              name = "*";
388              c = '=';
389            }
390          }
391          else
392          {
393            c = dnString.charAt(pos);
394          }
395        }
396      }
397
398
399      if (c == '=')
400      {
401        pos++;
402      }
403      else if (c == ',' || c == ';')
404      {
405        if (name.equals("*"))
406        {
407          rdnComponents.add(new PatternRDN(name, null, dnString));
408          pos++;
409          continue;
410        }
411        else if (name.equals("**"))
412        {
413          doubleWildPos.add(rdnComponents.size());
414          pos++;
415          continue;
416        }
417        else
418        {
419          pos = attributePos;
420          name = "*";
421        }
422      }
423      else
424      {
425        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
426            ERR_ATTR_SYNTAX_DN_NO_EQUAL.get(dnString, attributeName, c));
427      }
428
429      // Skip over any spaces after the equal sign.
430      while (pos < length && dnString.charAt(pos) == ' ')
431      {
432        pos++;
433      }
434
435
436      // If we are at the end of the DN string, then that must mean
437      // that the attribute value was empty.  This will probably never
438      // happen in a real-world environment, but technically isn't
439      // illegal.  If it does happen, then go ahead and create the
440      // RDN component and return the DN.
441      if (pos >= length)
442      {
443        ArrayList<ByteString> arrayList = newArrayList(ByteString.empty());
444        rdnComponents.add(new PatternRDN(name, arrayList, dnString));
445        break;
446      }
447
448
449      // Parse the value for this RDN component.
450      ArrayList<ByteString> parsedValue = new ArrayList<>();
451      pos = parseValuePattern(dnString, pos, parsedValue);
452
453
454      // Create the new RDN with the provided information.
455      PatternRDN rdn = new PatternRDN(name, parsedValue, dnString);
456
457
458      // Skip over any spaces that might be after the attribute value.
459      while (pos < length && ((c = dnString.charAt(pos)) == ' '))
460      {
461        pos++;
462      }
463
464
465      // Most likely, we will be at either the end of the RDN
466      // component or the end of the DN. If so, then handle that appropriately.
467      if (pos >= length)
468      {
469        // We're at the end of the DN string and should have a valid DN so return it.
470        rdnComponents.add(rdn);
471        break;
472      }
473      else if (c == ',' || c == ';')
474      {
475        // We're at the end of the RDN component, so add it to the list,
476        // skip over the comma/semicolon, and start on the next component.
477        rdnComponents.add(rdn);
478        pos++;
479        continue;
480      }
481      else if (c != '+')
482      {
483        // This should not happen.  At any rate, it's an illegal
484        // character, so throw an exception.
485        LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_CHAR.get(dnString, c, pos);
486        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
487      }
488
489
490      // If we have gotten here, then this must be a multi-valued RDN.
491      // In that case, parse the remaining attribute/value pairs and
492      // add them to the RDN that we've already created.
493      while (true)
494      {
495        // Skip over the plus sign and any spaces that may follow it
496        // before the next attribute name.
497        pos++;
498        while (pos < length && dnString.charAt(pos) == ' ')
499        {
500          pos++;
501        }
502
503
504        // Parse the attribute name from the DN string.
505        attributeName = new StringBuilder();
506        pos = parseAttributePattern(dnString, pos, attributeName);
507
508
509        // Make sure that we're not at the end of the DN string
510        // because that would be invalid.
511        if (pos >= length)
512        {
513          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
514              ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME.get(dnString, attributeName));
515        }
516
517
518        name = attributeName.toString();
519
520        // Skip over any spaces between the attribute name and its
521        // value.
522        c = dnString.charAt(pos);
523        while (c == ' ')
524        {
525          pos++;
526          if (pos >= length)
527          {
528            // This means that we hit the end of the value before
529            // finding a '='.  This is illegal because there is no
530            // attribute-value separator.
531            LocalizableMessage message =
532                ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME.get(dnString, name);
533            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
534                                         message);
535          }
536          else
537          {
538            c = dnString.charAt(pos);
539          }
540        }
541
542
543        // The next character must be an equal sign.  If it is not,
544        // then that's an error.
545        if (c == '=')
546        {
547          pos++;
548        }
549        else
550        {
551          LocalizableMessage message = ERR_ATTR_SYNTAX_DN_NO_EQUAL.get(dnString, name, c);
552          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
553                                       message);
554        }
555
556
557        // Skip over any spaces after the equal sign.
558        while (pos < length && ((c = dnString.charAt(pos)) == ' '))
559        {
560          pos++;
561        }
562
563
564        // If we are at the end of the DN string, then that must mean
565        // that the attribute value was empty.  This will probably
566        // never happen in a real-world environment, but technically
567        // isn't illegal.  If it does happen, then go ahead and create
568        // the RDN component and return the DN.
569        if (pos >= length)
570        {
571          ArrayList<ByteString> arrayList = newArrayList(ByteString.empty());
572          rdn.addValue(name, arrayList, dnString);
573          rdnComponents.add(rdn);
574          break;
575        }
576
577
578        // Parse the value for this RDN component.
579        parsedValue = new ArrayList<>();
580        pos = parseValuePattern(dnString, pos, parsedValue);
581
582
583        // Create the new RDN with the provided information.
584        rdn.addValue(name, parsedValue, dnString);
585
586
587        // Skip over any spaces that might be after the attribute value.
588        while (pos < length && ((c = dnString.charAt(pos)) == ' '))
589        {
590          pos++;
591        }
592
593
594        // Most likely, we will be at either the end of the RDN
595        // component or the end of the DN.  If so, then handle that appropriately.
596        if (pos >= length)
597        {
598          // We're at the end of the DN string and should have a valid
599          // DN so return it.
600          rdnComponents.add(rdn);
601          break;
602        }
603        else if (c == ',' || c == ';')
604        {
605          // We're at the end of the RDN component, so add it to the
606          // list, skip over the comma/semicolon, and start on the next component.
607          rdnComponents.add(rdn);
608          pos++;
609          break;
610        }
611        else if (c != '+')
612        {
613          // This should not happen.  At any rate, it's an illegal
614          // character, so throw an exception.
615          LocalizableMessage message =
616              ERR_ATTR_SYNTAX_DN_INVALID_CHAR.get(dnString, c, pos);
617          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message);
618        }
619      }
620    }
621
622    if (doubleWildPos.isEmpty())
623    {
624      PatternRDN[] patterns = new PatternRDN[rdnComponents.size()];
625      patterns = rdnComponents.toArray(patterns);
626      return new PatternDN(patterns);
627    }
628    else
629    {
630      PatternRDN[] subInitial = null;
631      PatternRDN[] subFinal = null;
632      List<PatternRDN[]> subAnyElements = new ArrayList<>();
633
634      int i = 0;
635      int numComponents = rdnComponents.size();
636
637      int to = doubleWildPos.get(i);
638      if (to != 0)
639      {
640        // Initial piece.
641        subInitial = new PatternRDN[to];
642        subInitial = rdnComponents.subList(0, to).toArray(subInitial);
643      }
644
645      int from;
646      for (; i < doubleWildPos.size() - 1; i++)
647      {
648        from = doubleWildPos.get(i);
649        to = doubleWildPos.get(i+1);
650        PatternRDN[] subAny = new PatternRDN[to-from];
651        subAny = rdnComponents.subList(from, to).toArray(subAny);
652        subAnyElements.add(subAny);
653      }
654
655      if (i < doubleWildPos.size())
656      {
657        from = doubleWildPos.get(i);
658        if (from != numComponents)
659        {
660          // Final piece.
661          subFinal = new PatternRDN[numComponents-from];
662          subFinal = rdnComponents.subList(from, numComponents).
663               toArray(subFinal);
664        }
665      }
666
667      return new PatternDN(subInitial, subAnyElements, subFinal);
668    }
669  }
670
671
672  /**
673   * Parses an attribute name pattern from the provided DN pattern string
674   * starting at the specified location.
675   *
676   * @param  dnString         The DN pattern string to be parsed.
677   * @param  pos              The position at which to start parsing
678   *                          the attribute name pattern.
679   * @param  attributeName    The buffer to which to append the parsed
680   *                          attribute name pattern.
681   *
682   * @return  The position of the first character that is not part of
683   *          the attribute name pattern.
684   *
685   * @throws  DirectoryException  If it was not possible to parse a
686   *                              valid attribute name pattern from the
687   *                              provided DN pattern string.
688   */
689  static int parseAttributePattern(String dnString, int pos,
690                                   StringBuilder attributeName)
691          throws DirectoryException
692  {
693    int length = dnString.length();
694
695
696    // Skip over any leading spaces.
697    if (pos < length)
698    {
699      while (dnString.charAt(pos) == ' ')
700      {
701        pos++;
702        if (pos == length)
703        {
704          // This means that the remainder of the DN was completely
705          // comprised of spaces.  If we have gotten here, then we
706          // know that there is at least one RDN component, and
707          // therefore the last non-space character of the DN must
708          // have been a comma. This is not acceptable.
709          LocalizableMessage message = ERR_ATTR_SYNTAX_DN_END_WITH_COMMA.get(dnString);
710          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
711                                       message);
712        }
713      }
714    }
715
716    // Next, we should find the attribute name for this RDN component.
717    boolean       checkForOID   = false;
718    boolean       endOfName     = false;
719    while (pos < length)
720    {
721      // To make the switch more efficient, we'll include all ASCII
722      // characters in the range of allowed values and then reject the
723      // ones that aren't allowed.
724      char c = dnString.charAt(pos);
725      switch (c)
726      {
727        case ' ':
728          // This should denote the end of the attribute name.
729          endOfName = true;
730          break;
731
732
733        case '!':
734        case '"':
735        case '#':
736        case '$':
737        case '%':
738        case '&':
739        case '\'':
740        case '(':
741        case ')':
742          // None of these are allowed in an attribute name or any
743          // character immediately following it.
744          LocalizableMessage message =
745              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
746          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
747                                       message);
748
749
750        case '*':
751          // Wildcard character.
752          attributeName.append(c);
753          break;
754
755        case '+':
756          // None of these are allowed in an attribute name or any
757          // character immediately following it.
758          message =
759              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
760          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
761                                       message);
762
763
764        case ',':
765          // This should denote the end of the attribute name.
766          endOfName = true;
767          break;
768
769        case '-':
770          // This will be allowed as long as it isn't the first
771          // character in the attribute name.
772          if (attributeName.length() > 0)
773          {
774            attributeName.append(c);
775          }
776          else
777          {
778            message =
779                ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_INITIAL_DASH.get(dnString);
780            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
781                                         message);
782          }
783          break;
784
785
786        case '.':
787          // The period could be allowed if the attribute name is
788          // actually expressed as an OID.  We'll accept it for now,
789          // but make sure to check it later.
790          attributeName.append(c);
791          checkForOID = true;
792          break;
793
794
795        case '/':
796          // This is not allowed in an attribute name or any character
797          // immediately following it.
798          message =
799              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
800          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
801                                       message);
802
803
804        case '0':
805        case '1':
806        case '2':
807        case '3':
808        case '4':
809        case '5':
810        case '6':
811        case '7':
812        case '8':
813        case '9':
814          // Digits are always allowed if they are not the first
815          // character. However, they may be allowed if they are the
816          // first character if the valid is an OID or if the
817          // attribute name exceptions option is enabled.  Therefore,
818          // we'll accept it now and check it later.
819          attributeName.append(c);
820          break;
821
822
823        case ':':
824          // Not allowed in an attribute name or any
825          // character immediately following it.
826          message =
827              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
828          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
829                                       message);
830
831
832        case ';': // NOTE:  attribute options are not allowed in a DN.
833          // This should denote the end of the attribute name.
834          endOfName = true;
835          break;
836
837        case '<':
838          // None of these are allowed in an attribute name or any
839          // character immediately following it.
840          message =
841              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
842          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
843                                       message);
844
845
846        case '=':
847          // This should denote the end of the attribute name.
848          endOfName = true;
849          break;
850
851
852        case '>':
853        case '?':
854        case '@':
855          // None of these are allowed in an attribute name or any
856          // character immediately following it.
857          message =
858              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
859          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
860                                       message);
861
862
863        case 'A':
864        case 'B':
865        case 'C':
866        case 'D':
867        case 'E':
868        case 'F':
869        case 'G':
870        case 'H':
871        case 'I':
872        case 'J':
873        case 'K':
874        case 'L':
875        case 'M':
876        case 'N':
877        case 'O':
878        case 'P':
879        case 'Q':
880        case 'R':
881        case 'S':
882        case 'T':
883        case 'U':
884        case 'V':
885        case 'W':
886        case 'X':
887        case 'Y':
888        case 'Z':
889          // These will always be allowed.
890          attributeName.append(c);
891          break;
892
893
894        case '[':
895        case '\\':
896        case ']':
897        case '^':
898          // None of these are allowed in an attribute name or any
899          // character immediately following it.
900          message =
901              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
902          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
903                                       message);
904
905
906        case '_':
907          attributeName.append(c);
908          break;
909
910
911        case '`':
912          // This is not allowed in an attribute name or any character
913          // immediately following it.
914          message =
915              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
916          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
917                                       message);
918
919
920        case 'a':
921        case 'b':
922        case 'c':
923        case 'd':
924        case 'e':
925        case 'f':
926        case 'g':
927        case 'h':
928        case 'i':
929        case 'j':
930        case 'k':
931        case 'l':
932        case 'm':
933        case 'n':
934        case 'o':
935        case 'p':
936        case 'q':
937        case 'r':
938        case 's':
939        case 't':
940        case 'u':
941        case 'v':
942        case 'w':
943        case 'x':
944        case 'y':
945        case 'z':
946          // These will always be allowed.
947          attributeName.append(c);
948          break;
949
950
951        default:
952          // This is not allowed in an attribute name or any character
953          // immediately following it.
954          message =
955              ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos);
956          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
957                                       message);
958      }
959
960
961      if (endOfName)
962      {
963        break;
964      }
965
966      pos++;
967    }
968
969
970    // We should now have the full attribute name.  However, we may
971    // still need to perform some validation, particularly if the
972    // name contains a period or starts with a digit.  It must also
973    // have at least one character.
974    if (attributeName.length() == 0)
975    {
976      LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(dnString);
977      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
978                                   message);
979    }
980    else if (checkForOID)
981    {
982      boolean validOID = true;
983
984      int namePos = 0;
985      int nameLength = attributeName.length();
986      char ch0 = attributeName.charAt(0);
987      if (ch0 == 'o' || ch0 == 'O')
988      {
989        if (nameLength <= 4)
990        {
991          validOID = false;
992        }
993        else
994        {
995          char ch1 = attributeName.charAt(1);
996          char ch2 = attributeName.charAt(2);
997          if ((ch1 == 'i' || ch1 == 'I')
998              && (ch2 == 'd' || ch2 == 'D')
999              && attributeName.charAt(3) == '.')
1000          {
1001            attributeName.delete(0, 4);
1002            nameLength -= 4;
1003          }
1004          else
1005          {
1006            validOID = false;
1007          }
1008        }
1009      }
1010
1011      while (validOID && namePos < nameLength)
1012      {
1013        char ch = attributeName.charAt(namePos++);
1014        if (isDigit(ch))
1015        {
1016          while (validOID && namePos < nameLength &&
1017                 isDigit(attributeName.charAt(namePos)))
1018          {
1019            namePos++;
1020          }
1021
1022          if (namePos < nameLength &&
1023              attributeName.charAt(namePos) != '.')
1024          {
1025            validOID = false;
1026          }
1027        }
1028        else if (ch == '.')
1029        {
1030          if (namePos == 1 ||
1031              attributeName.charAt(namePos-2) == '.')
1032          {
1033            validOID = false;
1034          }
1035        }
1036        else
1037        {
1038          validOID = false;
1039        }
1040      }
1041
1042
1043      if (validOID && attributeName.charAt(nameLength-1) == '.')
1044      {
1045        validOID = false;
1046      }
1047
1048      if (! validOID)
1049      {
1050        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1051            ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_PERIOD.get(dnString, attributeName));
1052      }
1053    }
1054
1055    return pos;
1056  }
1057
1058
1059  /**
1060   * Parses the attribute value pattern from the provided DN pattern
1061   * string starting at the specified location.  The value is split up
1062   * according to the wildcard locations, and the fragments are inserted
1063   * into the provided list.
1064   *
1065   * @param  dnString        The DN pattern string to be parsed.
1066   * @param  pos             The position of the first character in
1067   *                         the attribute value pattern to parse.
1068   * @param  attributeValues The list whose elements should be set to
1069   *                         the parsed attribute value fragments when
1070   *                         this method completes successfully.
1071   *
1072   * @return  The position of the first character that is not part of
1073   *          the attribute value.
1074   *
1075   * @throws  DirectoryException  If it was not possible to parse a
1076   *                              valid attribute value pattern from the
1077   *                              provided DN string.
1078   */
1079  private static int parseValuePattern(String dnString, int pos,
1080                                       ArrayList<ByteString> attributeValues)
1081          throws DirectoryException
1082  {
1083    // All leading spaces have already been stripped so we can start
1084    // reading the value.  However, it may be empty so check for that.
1085    int length = dnString.length();
1086    if (pos >= length)
1087    {
1088      return pos;
1089    }
1090
1091
1092    // Look at the first character.  If it is an octothorpe (#), then
1093    // that means that the value should be a hex string.
1094    char c = dnString.charAt(pos++);
1095    if (c == '#')
1096    {
1097      // The first two characters must be hex characters.
1098      StringBuilder hexString = new StringBuilder();
1099      if (pos+2 > length)
1100      {
1101        LocalizableMessage message = ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(dnString);
1102        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1103                                     message);
1104      }
1105
1106      for (int i=0; i < 2; i++)
1107      {
1108        c = dnString.charAt(pos++);
1109        if (isHexDigit(c))
1110        {
1111          hexString.append(c);
1112        }
1113        else
1114        {
1115          LocalizableMessage message =
1116              ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(dnString, c);
1117          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1118                                       message);
1119        }
1120      }
1121
1122
1123      // The rest of the value must be a multiple of two hex
1124      // characters.  The end of the value may be designated by the
1125      // end of the DN, a comma or semicolon, or a space.
1126      while (pos < length)
1127      {
1128        c = dnString.charAt(pos++);
1129        if (isHexDigit(c))
1130        {
1131          hexString.append(c);
1132
1133          if (pos < length)
1134          {
1135            c = dnString.charAt(pos++);
1136            if (isHexDigit(c))
1137            {
1138              hexString.append(c);
1139            }
1140            else
1141            {
1142              LocalizableMessage message =
1143                  ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(dnString, c);
1144              throw new DirectoryException(
1145                             ResultCode.INVALID_DN_SYNTAX, message);
1146            }
1147          }
1148          else
1149          {
1150            LocalizableMessage message =
1151                ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(dnString);
1152            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1153                                         message);
1154          }
1155        }
1156        else if (c == ' ' || c == ',' || c == ';')
1157        {
1158          // This denotes the end of the value.
1159          pos--;
1160          break;
1161        }
1162        else
1163        {
1164          LocalizableMessage message =
1165              ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(dnString, c);
1166          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1167                                       message);
1168        }
1169      }
1170
1171
1172      // At this point, we should have a valid hex string.  Convert it
1173      // to a byte array and set that as the value of the provided
1174      // octet string.
1175      try
1176      {
1177        byte[] bytes = hexStringToByteArray(hexString.toString());
1178        attributeValues.add(ByteString.wrap(bytes));
1179        return pos;
1180      }
1181      catch (Exception e)
1182      {
1183        logger.traceException(e);
1184        throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1185            ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(dnString, e));
1186      }
1187    }
1188
1189
1190    // If the first character is a quotation mark, then the value
1191    // should continue until the corresponding closing quotation mark.
1192    else if (c == '"')
1193    {
1194      // Keep reading until we find an unescaped closing quotation
1195      // mark.
1196      boolean escaped = false;
1197      StringBuilder valueString = new StringBuilder();
1198      while (true)
1199      {
1200        if (pos >= length)
1201        {
1202          // We hit the end of the DN before the closing quote.
1203          // That's an error.
1204          LocalizableMessage message = ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE.get(dnString);
1205          throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1206                                       message);
1207        }
1208
1209        c = dnString.charAt(pos++);
1210        if (escaped)
1211        {
1212          // The previous character was an escape, so we'll take this
1213          // one no matter what.
1214          valueString.append(c);
1215          escaped = false;
1216        }
1217        else if (c == '\\')
1218        {
1219          // The next character is escaped.  Set a flag to denote
1220          // this, but don't include the backslash.
1221          escaped = true;
1222        }
1223        else if (c == '"')
1224        {
1225          // This is the end of the value.
1226          break;
1227        }
1228        else
1229        {
1230          // This is just a regular character that should be in the
1231          // value.
1232          valueString.append(c);
1233        }
1234      }
1235
1236      attributeValues.add(ByteString.valueOfUtf8(valueString));
1237      return pos;
1238    }
1239
1240
1241    // Otherwise, use general parsing to find the end of the value.
1242    else
1243    {
1244      boolean escaped;
1245      StringBuilder valueString = new StringBuilder();
1246      StringBuilder hexChars    = new StringBuilder();
1247
1248      if (c == '\\')
1249      {
1250        escaped = true;
1251      }
1252      else if (c == '*')
1253      {
1254        escaped = false;
1255        attributeValues.add(ByteString.valueOfUtf8(valueString));
1256      }
1257      else
1258      {
1259        escaped = false;
1260        valueString.append(c);
1261      }
1262
1263
1264      // Keep reading until we find an unescaped comma or plus sign or
1265      // the end of the DN.
1266      while (true)
1267      {
1268        if (pos >= length)
1269        {
1270          // This is the end of the DN and therefore the end of the
1271          // value.  If there are any hex characters, then we need to
1272          // deal with them accordingly.
1273          appendHexChars(dnString, valueString, hexChars);
1274          break;
1275        }
1276
1277        c = dnString.charAt(pos++);
1278        if (escaped)
1279        {
1280          // The previous character was an escape, so we'll take this
1281          // one.  However, this could be a hex digit, and if that's
1282          // the case then the escape would actually be in front of
1283          // two hex digits that should be treated as a special
1284          // character.
1285          if (isHexDigit(c))
1286          {
1287            // It is a hexadecimal digit, so the next digit must be
1288            // one too.  However, this could be just one in a series
1289            // of escaped hex pairs that is used in a string
1290            // containing one or more multi-byte UTF-8 characters so
1291            // we can't just treat this byte in isolation.  Collect
1292            // all the bytes together and make sure to take care of
1293            // these hex bytes before appending anything else to the
1294            // value.
1295            if (pos >= length)
1296            {
1297              LocalizableMessage message =
1298                  ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(dnString);
1299              throw new DirectoryException(
1300                             ResultCode.INVALID_DN_SYNTAX, message);
1301            }
1302            else
1303            {
1304              char c2 = dnString.charAt(pos++);
1305              if (isHexDigit(c2))
1306              {
1307                hexChars.append(c);
1308                hexChars.append(c2);
1309              }
1310              else
1311              {
1312                LocalizableMessage message =
1313                    ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(dnString);
1314                throw new DirectoryException(
1315                               ResultCode.INVALID_DN_SYNTAX, message);
1316              }
1317            }
1318          }
1319          else
1320          {
1321            appendHexChars(dnString, valueString, hexChars);
1322            valueString.append(c);
1323          }
1324
1325          escaped = false;
1326        }
1327        else if (c == '\\')
1328        {
1329          escaped = true;
1330        }
1331        else if (c == ',' || c == ';')
1332        {
1333          appendHexChars(dnString, valueString, hexChars);
1334          pos--;
1335          break;
1336        }
1337        else if (c == '+')
1338        {
1339          appendHexChars(dnString, valueString, hexChars);
1340          pos--;
1341          break;
1342        }
1343        else if (c == '*')
1344        {
1345          appendHexChars(dnString, valueString, hexChars);
1346          if (valueString.length() == 0)
1347          {
1348            LocalizableMessage message =
1349                WARN_PATTERN_DN_CONSECUTIVE_WILDCARDS_IN_VALUE.get(dnString);
1350            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1351                                         message);
1352          }
1353          attributeValues.add(ByteString.valueOfUtf8(valueString));
1354          valueString = new StringBuilder();
1355          hexChars = new StringBuilder();
1356        }
1357        else
1358        {
1359          appendHexChars(dnString, valueString, hexChars);
1360          valueString.append(c);
1361        }
1362      }
1363
1364
1365      // Strip off any unescaped spaces that may be at the end of the
1366      // value.
1367      if (pos > 2 && dnString.charAt(pos-1) == ' ' &&
1368           dnString.charAt(pos-2) != '\\')
1369      {
1370        int lastPos = valueString.length() - 1;
1371        while (lastPos > 0)
1372        {
1373          if (valueString.charAt(lastPos) == ' ')
1374          {
1375            valueString.delete(lastPos, lastPos+1);
1376            lastPos--;
1377          }
1378          else
1379          {
1380            break;
1381          }
1382        }
1383      }
1384
1385
1386      attributeValues.add(ByteString.valueOfUtf8(valueString));
1387      return pos;
1388    }
1389  }
1390
1391
1392  /**
1393   * Decodes a hexadecimal string from the provided
1394   * <CODE>hexChars</CODE> buffer, converts it to a byte array, and
1395   * then converts that to a UTF-8 string.  The resulting UTF-8 string
1396   * will be appended to the provided <CODE>valueString</CODE> buffer,
1397   * and the <CODE>hexChars</CODE> buffer will be cleared.
1398   *
1399   * @param  dnString     The DN string that is being decoded.
1400   * @param  valueString  The buffer containing the value to which the
1401   *                      decoded string should be appended.
1402   * @param  hexChars     The buffer containing the hexadecimal
1403   *                      characters to decode to a UTF-8 string.
1404   *
1405   * @throws  DirectoryException  If any problem occurs during the
1406   *                              decoding process.
1407   */
1408  private static void appendHexChars(String dnString,
1409                                     StringBuilder valueString,
1410                                     StringBuilder hexChars)
1411          throws DirectoryException
1412  {
1413    try
1414    {
1415      byte[] hexBytes = hexStringToByteArray(hexChars.toString());
1416      valueString.append(new String(hexBytes, "UTF-8"));
1417      hexChars.delete(0, hexChars.length());
1418    }
1419    catch (Exception e)
1420    {
1421      logger.traceException(e);
1422      throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX,
1423          ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(dnString, e));
1424    }
1425  }
1426}