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