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