001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2007-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.schema;
037
038
039
040import java.io.Serializable;
041import java.nio.ByteBuffer;
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.Map;
045
046import com.unboundid.ldap.sdk.LDAPException;
047import com.unboundid.ldap.sdk.ResultCode;
048import com.unboundid.util.NotExtensible;
049import com.unboundid.util.NotNull;
050import com.unboundid.util.Nullable;
051import com.unboundid.util.StaticUtils;
052import com.unboundid.util.ThreadSafety;
053import com.unboundid.util.ThreadSafetyLevel;
054
055import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
056
057
058
059/**
060 * This class provides a superclass for all schema element types, and defines a
061 * number of utility methods that may be used when parsing schema element
062 * strings.
063 */
064@NotExtensible()
065@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE)
066public abstract class SchemaElement
067       implements Serializable
068{
069  /**
070   * Indicates whether schema elements will be permitted to use an empty
071   * quoted string as the value of the {@code DESC} component.
072   */
073  private static boolean allowEmptyDescription = Boolean.getBoolean(
074         "com.unboundid.ldap.sdk.schema.AllowEmptyDescription");
075
076
077
078  /**
079   * The serial version UID for this serializable class.
080   */
081  private static final long serialVersionUID = -8249972237068748580L;
082
083
084
085  /**
086   * Indicates whether to allow schema elements to contain an empty string as
087   * the value for the {@code DESC} component.  Although quoted strings are not
088   * allowed in schema elements as per RFC 4512 section 4.1, some directory
089   * servers allow it, and it may be necessary to support schema definitions
090   * used in conjunction with those servers.
091   * <BR><BR>
092   * The LDAP SDK does not allow empty schema element descriptions by default,
093   * but it may be updated to allow it using either the
094   * {@link #setAllowEmptyDescription} method or by setting the value of the
095   * {@code com.unboundid.ldap.sdk.schema.AllowEmptyDescription} system property
096   * to {@code true} before this class is loaded.
097   *
098   * @return  {@code true} if the LDAP SDK should allow schema elements with
099   *          empty descriptions, or {@code false} if not.
100   */
101  public static boolean allowEmptyDescription()
102  {
103    return allowEmptyDescription;
104  }
105
106
107
108  /**
109   * Specifies whether to allow schema elements to contain an empty string as
110   * the value for the {@code DESC} component.  If specified, this will override
111   * the value of the
112   * {@code com.unboundid.ldap.sdk.schema.AllowEmptyDescription} system
113   * property.
114   *
115   * @param  allowEmptyDescription  Indicates whether to allow schema elements
116   *                                to contain an empty string as the value for
117   *                                the {@code DESC} component.
118   */
119  public static void setAllowEmptyDescription(
120                          final boolean allowEmptyDescription)
121  {
122    SchemaElement.allowEmptyDescription = allowEmptyDescription;
123  }
124
125
126
127  /**
128   * Skips over any any spaces in the provided string.
129   *
130   * @param  s         The string in which to skip the spaces.
131   * @param  startPos  The position at which to start skipping spaces.
132   * @param  length    The position of the end of the string.
133   *
134   * @return  The position of the next non-space character in the string.
135   *
136   * @throws  LDAPException  If the end of the string was reached without
137   *                         finding a non-space character.
138   */
139  static int skipSpaces(@NotNull final String s, final int startPos,
140                        final int length)
141         throws LDAPException
142  {
143    int pos = startPos;
144    while ((pos < length) && (s.charAt(pos) == ' '))
145    {
146      pos++;
147    }
148
149    if (pos >= length)
150    {
151      throw new LDAPException(ResultCode.DECODING_ERROR,
152           ERR_SCHEMA_ELEM_SKIP_SPACES_NO_CLOSE_PAREN.get(s));
153    }
154
155    return pos;
156  }
157
158
159
160  /**
161   * Reads one or more hex-encoded bytes from the specified portion of the RDN
162   * string.
163   *
164   * @param  s              The string from which the data is to be read.
165   * @param  startPos       The position at which to start reading.  This should
166   *                        be the first hex character immediately after the
167   *                        initial backslash.
168   * @param  length         The position of the end of the string.
169   * @param  componentName  The name of the component in the schema element
170   *                        definition whose value is being read.
171   * @param  buffer         The buffer to which the decoded string portion
172   *                        should be appended.
173   *
174   * @return  The position at which the caller may resume parsing.
175   *
176   * @throws  LDAPException  If a problem occurs while reading hex-encoded
177   *                         bytes.
178   */
179  private static int readEscapedHexString(@NotNull final String s,
180                                          final int startPos,
181                                          final int length,
182                                          @NotNull final String componentName,
183                                          @NotNull final StringBuilder buffer)
184          throws LDAPException
185  {
186    int pos    = startPos;
187
188    final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos);
189    while (pos < length)
190    {
191      final byte b;
192      switch (s.charAt(pos++))
193      {
194        case '0':
195          b = 0x00;
196          break;
197        case '1':
198          b = 0x10;
199          break;
200        case '2':
201          b = 0x20;
202          break;
203        case '3':
204          b = 0x30;
205          break;
206        case '4':
207          b = 0x40;
208          break;
209        case '5':
210          b = 0x50;
211          break;
212        case '6':
213          b = 0x60;
214          break;
215        case '7':
216          b = 0x70;
217          break;
218        case '8':
219          b = (byte) 0x80;
220          break;
221        case '9':
222          b = (byte) 0x90;
223          break;
224        case 'a':
225        case 'A':
226          b = (byte) 0xA0;
227          break;
228        case 'b':
229        case 'B':
230          b = (byte) 0xB0;
231          break;
232        case 'c':
233        case 'C':
234          b = (byte) 0xC0;
235          break;
236        case 'd':
237        case 'D':
238          b = (byte) 0xD0;
239          break;
240        case 'e':
241        case 'E':
242          b = (byte) 0xE0;
243          break;
244        case 'f':
245        case 'F':
246          b = (byte) 0xF0;
247          break;
248        default:
249          throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
250               ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s, s.charAt(pos-1),
251                    (pos-1), componentName));
252      }
253
254      if (pos >= length)
255      {
256        throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
257             ERR_SCHEMA_ELEM_MISSING_HEX_CHAR.get(s, componentName));
258      }
259
260      switch (s.charAt(pos++))
261      {
262        case '0':
263          byteBuffer.put(b);
264          break;
265        case '1':
266          byteBuffer.put((byte) (b | 0x01));
267          break;
268        case '2':
269          byteBuffer.put((byte) (b | 0x02));
270          break;
271        case '3':
272          byteBuffer.put((byte) (b | 0x03));
273          break;
274        case '4':
275          byteBuffer.put((byte) (b | 0x04));
276          break;
277        case '5':
278          byteBuffer.put((byte) (b | 0x05));
279          break;
280        case '6':
281          byteBuffer.put((byte) (b | 0x06));
282          break;
283        case '7':
284          byteBuffer.put((byte) (b | 0x07));
285          break;
286        case '8':
287          byteBuffer.put((byte) (b | 0x08));
288          break;
289        case '9':
290          byteBuffer.put((byte) (b | 0x09));
291          break;
292        case 'a':
293        case 'A':
294          byteBuffer.put((byte) (b | 0x0A));
295          break;
296        case 'b':
297        case 'B':
298          byteBuffer.put((byte) (b | 0x0B));
299          break;
300        case 'c':
301        case 'C':
302          byteBuffer.put((byte) (b | 0x0C));
303          break;
304        case 'd':
305        case 'D':
306          byteBuffer.put((byte) (b | 0x0D));
307          break;
308        case 'e':
309        case 'E':
310          byteBuffer.put((byte) (b | 0x0E));
311          break;
312        case 'f':
313        case 'F':
314          byteBuffer.put((byte) (b | 0x0F));
315          break;
316        default:
317          throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
318               ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s, s.charAt(pos-1),
319                    (pos-1), componentName));
320      }
321
322      if (((pos+1) < length) && (s.charAt(pos) == '\\') &&
323          StaticUtils.isHex(s.charAt(pos+1)))
324      {
325        // It appears that there are more hex-encoded bytes to follow, so keep
326        // reading.
327        pos++;
328        continue;
329      }
330      else
331      {
332        break;
333      }
334    }
335
336    byteBuffer.flip();
337    final byte[] byteArray = new byte[byteBuffer.limit()];
338    byteBuffer.get(byteArray);
339    buffer.append(StaticUtils.toUTF8String(byteArray));
340    return pos;
341  }
342
343
344
345  /**
346   * Reads a single-quoted string from the provided string.
347   *
348   * @param  s              The string from which to read the single-quoted
349   *                        string.
350   * @param  startPos       The position at which to start reading.
351   * @param  length         The position of the end of the string.
352   * @param  componentName  The name of the component in the schema element
353   *                        definition whose value is being read.
354   * @param  buffer         The buffer into which the single-quoted string
355   *                        should be placed (without the surrounding single
356   *                        quotes).
357   *
358   * @return  The position of the first space immediately following the closing
359   *          quote.
360   *
361   * @throws  LDAPException  If a problem is encountered while attempting to
362   *                         read the single-quoted string.
363   */
364  static int readQDString(@NotNull final String s, final int startPos,
365                          final int length, @NotNull final String componentName,
366                          @NotNull final StringBuilder buffer)
367      throws LDAPException
368  {
369    // The first character must be a single quote.
370    if (s.charAt(startPos) != '\'')
371    {
372      throw new LDAPException(ResultCode.DECODING_ERROR,
373           ERR_SCHEMA_ELEM_EXPECTED_SINGLE_QUOTE.get(s, startPos,
374                componentName));
375    }
376
377    // Read until we find the next closing quote.  If we find any hex-escaped
378    // characters along the way, then decode them.
379    int pos = startPos + 1;
380    while (pos < length)
381    {
382      final char c = s.charAt(pos++);
383      if (c == '\'')
384      {
385        // This is the end of the quoted string.
386        break;
387      }
388      else if (c == '\\')
389      {
390        // This designates the beginning of one or more hex-encoded bytes.
391        if (pos >= length)
392        {
393          throw new LDAPException(ResultCode.DECODING_ERROR,
394               ERR_SCHEMA_ELEM_ENDS_WITH_BACKSLASH.get(s, componentName));
395        }
396
397        pos = readEscapedHexString(s, pos, length, componentName, buffer);
398      }
399      else
400      {
401        buffer.append(c);
402      }
403    }
404
405    if ((pos >= length) || ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
406    {
407      throw new LDAPException(ResultCode.DECODING_ERROR,
408           ERR_SCHEMA_ELEM_NO_CLOSING_PAREN.get(s, componentName));
409    }
410
411    if (buffer.length() == 0)
412    {
413      if (! (allowEmptyDescription && componentName.equalsIgnoreCase("DESC")))
414      {
415        throw new LDAPException(ResultCode.DECODING_ERROR,
416             ERR_SCHEMA_ELEM_EMPTY_QUOTES.get(s, componentName));
417      }
418    }
419
420    return pos;
421  }
422
423
424
425  /**
426   * Reads one a set of one or more single-quoted strings from the provided
427   * string.  The value to read may be either a single string enclosed in
428   * single quotes, or an opening parenthesis followed by a space followed by
429   * one or more space-delimited single-quoted strings, followed by a space and
430   * a closing parenthesis.
431   *
432   * @param  s              The string from which to read the single-quoted
433   *                        strings.
434   * @param  startPos       The position at which to start reading.
435   * @param  length         The position of the end of the string.
436   * @param  componentName  The name of the component in the schema element
437   *                        definition whose value is being read.
438   * @param  valueList      The list into which the values read may be placed.
439   *
440   * @return  The position of the first space immediately following the end of
441   *          the values.
442   *
443   * @throws  LDAPException  If a problem is encountered while attempting to
444   *                         read the single-quoted strings.
445   */
446  static int readQDStrings(@NotNull final String s, final int startPos,
447                           final int length,
448                           @NotNull final String componentName,
449                           @NotNull final ArrayList<String> valueList)
450      throws LDAPException
451  {
452    // Look at the first character.  It must be either a single quote or an
453    // opening parenthesis.
454    char c = s.charAt(startPos);
455    if (c == '\'')
456    {
457      // It's just a single value, so use the readQDString method to get it.
458      final StringBuilder buffer = new StringBuilder();
459      final int returnPos = readQDString(s, startPos, length, componentName,
460           buffer);
461      valueList.add(buffer.toString());
462      return returnPos;
463    }
464    else if (c == '(')
465    {
466      int pos = startPos + 1;
467      while (true)
468      {
469        pos = skipSpaces(s, pos, length);
470        c = s.charAt(pos);
471        if (c == ')')
472        {
473          // This is the end of the value list.
474          pos++;
475          break;
476        }
477        else if (c == '\'')
478        {
479          // This is the next value in the list.
480          final StringBuilder buffer = new StringBuilder();
481          pos = readQDString(s, pos, length, componentName, buffer);
482          valueList.add(buffer.toString());
483        }
484        else
485        {
486          throw new LDAPException(ResultCode.DECODING_ERROR,
487               ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s, startPos,
488                    componentName));
489        }
490      }
491
492      if (valueList.isEmpty())
493      {
494        throw new LDAPException(ResultCode.DECODING_ERROR,
495             ERR_SCHEMA_ELEM_EMPTY_STRING_LIST.get(s, componentName));
496      }
497
498      if ((pos >= length) ||
499          ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
500      {
501        throw new LDAPException(ResultCode.DECODING_ERROR,
502             ERR_SCHEMA_ELEM_NO_SPACE_AFTER_QUOTE.get(s, componentName));
503      }
504
505      return pos;
506    }
507    else
508    {
509      throw new LDAPException(ResultCode.DECODING_ERROR,
510           ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s, startPos,
511                componentName));
512    }
513  }
514
515
516
517  /**
518   * Reads an OID value from the provided string.  The OID value may be either a
519   * numeric OID or a string name.  This implementation will be fairly lenient
520   * with regard to the set of characters that may be present, and it will
521   * allow the OID to be enclosed in single quotes.
522   *
523   * @param  s         The string from which to read the OID string.
524   * @param  startPos  The position at which to start reading.
525   * @param  length    The position of the end of the string.
526   * @param  buffer    The buffer into which the OID string should be placed.
527   *
528   * @return  The position of the first space immediately following the OID
529   *          string.
530   *
531   * @throws  LDAPException  If a problem is encountered while attempting to
532   *                         read the OID string.
533   */
534  static int readOID(@NotNull final String s, final int startPos,
535                     final int length, @NotNull final StringBuilder buffer)
536      throws LDAPException
537  {
538    // Read until we find the first space.
539    int pos = startPos;
540    boolean lastWasQuote = false;
541    while (pos < length)
542    {
543      final char c = s.charAt(pos);
544      if ((c == ' ') || (c == '$') || (c == ')'))
545      {
546        if (buffer.length() == 0)
547        {
548          throw new LDAPException(ResultCode.DECODING_ERROR,
549               ERR_SCHEMA_ELEM_EMPTY_OID.get(s));
550        }
551
552        return pos;
553      }
554      else if (((c >= 'a') && (c <= 'z')) ||
555               ((c >= 'A') && (c <= 'Z')) ||
556               ((c >= '0') && (c <= '9')) ||
557               (c == '-') || (c == '.') || (c == '_') ||
558               (c == '{') || (c == '}'))
559      {
560        if (lastWasQuote)
561        {
562          throw new LDAPException(ResultCode.DECODING_ERROR,
563               ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, (pos-1)));
564        }
565
566        buffer.append(c);
567      }
568      else if (c == '\'')
569      {
570        if (buffer.length() != 0)
571        {
572          lastWasQuote = true;
573        }
574      }
575      else
576      {
577          throw new LDAPException(ResultCode.DECODING_ERROR,
578               ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, pos));
579      }
580
581      pos++;
582    }
583
584
585    // We hit the end of the string before finding a space.
586    throw new LDAPException(ResultCode.DECODING_ERROR,
587         ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID.get(s));
588  }
589
590
591
592  /**
593   * Reads one a set of one or more OID strings from the provided string.  The
594   * value to read may be either a single OID string or an opening parenthesis
595   * followed by a space followed by one or more space-delimited OID strings,
596   * followed by a space and a closing parenthesis.
597   *
598   * @param  s              The string from which to read the OID strings.
599   * @param  startPos       The position at which to start reading.
600   * @param  length         The position of the end of the string.
601   * @param  componentName  The name of the component in the schema element
602   *                        definition whose value is being read.
603   * @param  valueList      The list into which the values read may be placed.
604   *
605   * @return  The position of the first space immediately following the end of
606   *          the values.
607   *
608   * @throws  LDAPException  If a problem is encountered while attempting to
609   *                         read the OID strings.
610   */
611  static int readOIDs(@NotNull final String s, final int startPos,
612                      final int length, @NotNull final String componentName,
613                      @NotNull final ArrayList<String> valueList)
614      throws LDAPException
615  {
616    // Look at the first character.  If it's an opening parenthesis, then read
617    // a list of OID strings.  Otherwise, just read a single string.
618    char c = s.charAt(startPos);
619    if (c == '(')
620    {
621      int pos = startPos + 1;
622      while (true)
623      {
624        pos = skipSpaces(s, pos, length);
625        c = s.charAt(pos);
626        if (c == ')')
627        {
628          // This is the end of the value list.
629          pos++;
630          break;
631        }
632        else if (c == '$')
633        {
634          // This is the delimiter before the next value in the list.
635          pos++;
636          pos = skipSpaces(s, pos, length);
637          final StringBuilder buffer = new StringBuilder();
638          pos = readOID(s, pos, length, buffer);
639          valueList.add(buffer.toString());
640        }
641        else if (valueList.isEmpty())
642        {
643          // This is the first value in the list.
644          final StringBuilder buffer = new StringBuilder();
645          pos = readOID(s, pos, length, buffer);
646          valueList.add(buffer.toString());
647        }
648        else
649        {
650          throw new LDAPException(ResultCode.DECODING_ERROR,
651               ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID_LIST.get(s, pos,
652                    componentName));
653        }
654      }
655
656      if (valueList.isEmpty())
657      {
658        throw new LDAPException(ResultCode.DECODING_ERROR,
659             ERR_SCHEMA_ELEM_EMPTY_OID_LIST.get(s, componentName));
660      }
661
662      if (pos >= length)
663      {
664        // Technically, there should be a space after the closing parenthesis,
665        // but there are known cases in which servers (like Active Directory)
666        // omit this space, so we'll be lenient and allow a missing space.  But
667        // it can't possibly be the end of the schema element definition, so
668        // that's still an error.
669        throw new LDAPException(ResultCode.DECODING_ERROR,
670             ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID_LIST.get(s, componentName));
671      }
672
673      return pos;
674    }
675    else
676    {
677      final StringBuilder buffer = new StringBuilder();
678      final int returnPos = readOID(s, startPos, length, buffer);
679      valueList.add(buffer.toString());
680      return returnPos;
681    }
682  }
683
684
685
686  /**
687   * Appends a properly-encoded representation of the provided value to the
688   * given buffer.
689   *
690   * @param  value   The value to be encoded and placed in the buffer.
691   * @param  buffer  The buffer to which the encoded value is to be appended.
692   */
693  static void encodeValue(@NotNull final String value,
694                          @NotNull final StringBuilder buffer)
695  {
696    final int length = value.length();
697    for (int i=0; i < length; i++)
698    {
699      final char c = value.charAt(i);
700      if ((c < ' ') || (c > '~') || (c == '\\') || (c == '\''))
701      {
702        StaticUtils.hexEncode(c, buffer);
703      }
704      else
705      {
706        buffer.append(c);
707      }
708    }
709  }
710
711
712
713  /**
714   * Retrieves the type of schema element that this object represents.
715   *
716   * @return  The type of schema element that this object represents.
717   */
718  @NotNull()
719  public abstract SchemaElementType getSchemaElementType();
720
721
722
723  /**
724   * Retrieves a hash code for this schema element.
725   *
726   * @return  A hash code for this schema element.
727   */
728  public abstract int hashCode();
729
730
731
732  /**
733   * Indicates whether the provided object is equal to this schema element.
734   *
735   * @param  o  The object for which to make the determination.
736   *
737   * @return  {@code true} if the provided object may be considered equal to
738   *          this schema element, or {@code false} if not.
739   */
740  public abstract boolean equals(@Nullable Object o);
741
742
743
744  /**
745   * Indicates whether the two extension maps are equivalent.
746   *
747   * @param  m1  The first schema element to examine.
748   * @param  m2  The second schema element to examine.
749   *
750   * @return  {@code true} if the provided extension maps are equivalent, or
751   *          {@code false} if not.
752   */
753  protected static boolean extensionsEqual(
754                                @NotNull final Map<String,String[]> m1,
755                                @NotNull final Map<String,String[]> m2)
756  {
757    if (m1.isEmpty())
758    {
759      return m2.isEmpty();
760    }
761
762    if (m1.size() != m2.size())
763    {
764      return false;
765    }
766
767    for (final Map.Entry<String,String[]> e : m1.entrySet())
768    {
769      final String[] v1 = e.getValue();
770      final String[] v2 = m2.get(e.getKey());
771      if (! StaticUtils.arraysEqualOrderIndependent(v1, v2))
772      {
773        return false;
774      }
775    }
776
777    return true;
778  }
779
780
781
782  /**
783   * Converts the provided collection of strings to an array.
784   *
785   * @param  c  The collection to convert to an array.  It may be {@code null}.
786   *
787   * @return  A string array if the provided collection is non-{@code null}, or
788   *          {@code null} if the provided collection is {@code null}.
789   */
790  @Nullable()
791  static String[] toArray(@Nullable final Collection<String> c)
792  {
793    if (c == null)
794    {
795      return null;
796    }
797
798    return c.toArray(StaticUtils.NO_STRINGS);
799  }
800
801
802
803  /**
804   * Retrieves a string representation of this schema element, in the format
805   * described in RFC 4512.
806   *
807   * @return  A string representation of this schema element, in the format
808   *          described in RFC 4512.
809   */
810  @Override()
811  @NotNull()
812  public abstract String toString();
813}