View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.util.internal;
17  
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Iterator;
22  import java.util.List;
23  
24  import static io.netty.util.internal.ObjectUtil.*;
25  
26  /**
27   * String utility class.
28   */
29  public final class StringUtil {
30  
31      public static final String EMPTY_STRING = "";
32      public static final String NEWLINE = SystemPropertyUtil.get("line.separator", "\n");
33  
34      public static final char DOUBLE_QUOTE = '\"';
35      public static final char COMMA = ',';
36      public static final char LINE_FEED = '\n';
37      public static final char CARRIAGE_RETURN = '\r';
38      public static final char TAB = '\t';
39      public static final char SPACE = 0x20;
40  
41      private static final String[] BYTE2HEX_PAD = new String[256];
42      private static final String[] BYTE2HEX_NOPAD = new String[256];
43      private static final byte[] HEX2B;
44  
45      /**
46       * 2 - Quote character at beginning and end.
47       * 5 - Extra allowance for anticipated escape characters that may be added.
48       */
49      private static final int CSV_NUMBER_ESCAPE_CHARACTERS = 2 + 5;
50      private static final char PACKAGE_SEPARATOR_CHAR = '.';
51  
52      static {
53          // Generate the lookup table that converts a byte into a 2-digit hexadecimal integer.
54          for (int i = 0; i < BYTE2HEX_PAD.length; i++) {
55              String str = Integer.toHexString(i);
56              BYTE2HEX_PAD[i] = i > 0xf ? str : ('0' + str);
57              BYTE2HEX_NOPAD[i] = str;
58          }
59          // Generate the lookup table that converts an hex char into its decimal value:
60          // the size of the table is such that the JVM is capable of save any bounds-check
61          // if a char type is used as an index.
62          HEX2B = new byte[Character.MAX_VALUE + 1];
63          Arrays.fill(HEX2B, (byte) -1);
64          HEX2B['0'] = 0;
65          HEX2B['1'] = 1;
66          HEX2B['2'] = 2;
67          HEX2B['3'] = 3;
68          HEX2B['4'] = 4;
69          HEX2B['5'] = 5;
70          HEX2B['6'] = 6;
71          HEX2B['7'] = 7;
72          HEX2B['8'] = 8;
73          HEX2B['9'] = 9;
74          HEX2B['A'] = 10;
75          HEX2B['B'] = 11;
76          HEX2B['C'] = 12;
77          HEX2B['D'] = 13;
78          HEX2B['E'] = 14;
79          HEX2B['F'] = 15;
80          HEX2B['a'] = 10;
81          HEX2B['b'] = 11;
82          HEX2B['c'] = 12;
83          HEX2B['d'] = 13;
84          HEX2B['e'] = 14;
85          HEX2B['f'] = 15;
86      }
87  
88      private StringUtil() {
89          // Unused.
90      }
91  
92      /**
93       * Get the item after one char delim if the delim is found (else null).
94       * This operation is a simplified and optimized
95       * version of {@link String#split(String, int)}.
96       */
97      public static String substringAfter(String value, char delim) {
98          int pos = value.indexOf(delim);
99          if (pos >= 0) {
100             return value.substring(pos + 1);
101         }
102         return null;
103     }
104 
105     /**
106      * Get the item before one char delim if the delim is found (else null).
107      * This operation is a simplified and optimized
108      * version of {@link String#split(String, int)}.
109      */
110     public static String substringBefore(String value, char delim) {
111         int pos = value.indexOf(delim);
112         if (pos >= 0) {
113             return value.substring(0, pos);
114         }
115         return null;
116     }
117 
118     /**
119      * Checks if two strings have the same suffix of specified length
120      *
121      * @param s   string
122      * @param p   string
123      * @param len length of the common suffix
124      * @return true if both s and p are not null and both have the same suffix. Otherwise - false
125      */
126     public static boolean commonSuffixOfLength(String s, String p, int len) {
127         return s != null && p != null && len >= 0 && s.regionMatches(s.length() - len, p, p.length() - len, len);
128     }
129 
130     /**
131      * Converts the specified byte value into a 2-digit hexadecimal integer.
132      */
133     public static String byteToHexStringPadded(int value) {
134         return BYTE2HEX_PAD[value & 0xff];
135     }
136 
137     /**
138      * Converts the specified byte value into a 2-digit hexadecimal integer and appends it to the specified buffer.
139      */
140     public static <T extends Appendable> T byteToHexStringPadded(T buf, int value) {
141         try {
142             buf.append(byteToHexStringPadded(value));
143         } catch (IOException e) {
144             PlatformDependent.throwException(e);
145         }
146         return buf;
147     }
148 
149     /**
150      * Converts the specified byte array into a hexadecimal value.
151      */
152     public static String toHexStringPadded(byte[] src) {
153         return toHexStringPadded(src, 0, src.length);
154     }
155 
156     /**
157      * Converts the specified byte array into a hexadecimal value.
158      */
159     public static String toHexStringPadded(byte[] src, int offset, int length) {
160         return toHexStringPadded(new StringBuilder(length << 1), src, offset, length).toString();
161     }
162 
163     /**
164      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
165      */
166     public static <T extends Appendable> T toHexStringPadded(T dst, byte[] src) {
167         return toHexStringPadded(dst, src, 0, src.length);
168     }
169 
170     /**
171      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
172      */
173     public static <T extends Appendable> T toHexStringPadded(T dst, byte[] src, int offset, int length) {
174         final int end = offset + length;
175         for (int i = offset; i < end; i++) {
176             byteToHexStringPadded(dst, src[i]);
177         }
178         return dst;
179     }
180 
181     /**
182      * Converts the specified byte value into a hexadecimal integer.
183      */
184     public static String byteToHexString(int value) {
185         return BYTE2HEX_NOPAD[value & 0xff];
186     }
187 
188     /**
189      * Converts the specified byte value into a hexadecimal integer and appends it to the specified buffer.
190      */
191     public static <T extends Appendable> T byteToHexString(T buf, int value) {
192         try {
193             buf.append(byteToHexString(value));
194         } catch (IOException e) {
195             PlatformDependent.throwException(e);
196         }
197         return buf;
198     }
199 
200     /**
201      * Converts the specified byte array into a hexadecimal value.
202      */
203     public static String toHexString(byte[] src) {
204         return toHexString(src, 0, src.length);
205     }
206 
207     /**
208      * Converts the specified byte array into a hexadecimal value.
209      */
210     public static String toHexString(byte[] src, int offset, int length) {
211         return toHexString(new StringBuilder(length << 1), src, offset, length).toString();
212     }
213 
214     /**
215      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
216      */
217     public static <T extends Appendable> T toHexString(T dst, byte[] src) {
218         return toHexString(dst, src, 0, src.length);
219     }
220 
221     /**
222      * Converts the specified byte array into a hexadecimal value and appends it to the specified buffer.
223      */
224     public static <T extends Appendable> T toHexString(T dst, byte[] src, int offset, int length) {
225         assert length >= 0;
226         if (length == 0) {
227             return dst;
228         }
229 
230         final int end = offset + length;
231         final int endMinusOne = end - 1;
232         int i;
233 
234         // Skip preceding zeroes.
235         for (i = offset; i < endMinusOne; i++) {
236             if (src[i] != 0) {
237                 break;
238             }
239         }
240 
241         byteToHexString(dst, src[i++]);
242         int remaining = end - i;
243         toHexStringPadded(dst, src, i, remaining);
244 
245         return dst;
246     }
247 
248     /**
249      * Helper to decode half of a hexadecimal number from a string.
250      * @param c The ASCII character of the hexadecimal number to decode.
251      * Must be in the range {@code [0-9a-fA-F]}.
252      * @return The hexadecimal value represented in the ASCII character
253      * given, or {@code -1} if the character is invalid.
254      */
255     public static int decodeHexNibble(final char c) {
256         // Character.digit() is not used here, as it addresses a larger
257         // set of characters (both ASCII and full-width latin letters).
258         return HEX2B[c];
259     }
260 
261     /**
262      * Helper to decode half of a hexadecimal number from a string.
263      * @param b The ASCII character of the hexadecimal number to decode.
264      * Must be in the range {@code [0-9a-fA-F]}.
265      * @return The hexadecimal value represented in the ASCII character
266      * given, or {@code -1} if the character is invalid.
267      */
268     public static int decodeHexNibble(final byte b) {
269         // Character.digit() is not used here, as it addresses a larger
270         // set of characters (both ASCII and full-width latin letters).
271         return HEX2B[b];
272     }
273 
274     /**
275      * Decode a 2-digit hex byte from within a string.
276      */
277     public static byte decodeHexByte(CharSequence s, int pos) {
278         int hi = decodeHexNibble(s.charAt(pos));
279         int lo = decodeHexNibble(s.charAt(pos + 1));
280         if (hi == -1 || lo == -1) {
281             throw new IllegalArgumentException(String.format(
282                     "invalid hex byte '%s' at index %d of '%s'", s.subSequence(pos, pos + 2), pos, s));
283         }
284         return (byte) ((hi << 4) + lo);
285     }
286 
287     /**
288      * Decodes part of a string with <a href="https://en.wikipedia.org/wiki/Hex_dump">hex dump</a>
289      *
290      * @param hexDump a {@link CharSequence} which contains the hex dump
291      * @param fromIndex start of hex dump in {@code hexDump}
292      * @param length hex string length
293      */
294     public static byte[] decodeHexDump(CharSequence hexDump, int fromIndex, int length) {
295         if (length < 0 || (length & 1) != 0) {
296             throw new IllegalArgumentException("length: " + length);
297         }
298         if (length == 0) {
299             return EmptyArrays.EMPTY_BYTES;
300         }
301         byte[] bytes = new byte[length >>> 1];
302         for (int i = 0; i < length; i += 2) {
303             bytes[i >>> 1] = decodeHexByte(hexDump, fromIndex + i);
304         }
305         return bytes;
306     }
307 
308     /**
309      * Decodes a <a href="https://en.wikipedia.org/wiki/Hex_dump">hex dump</a>
310      */
311     public static byte[] decodeHexDump(CharSequence hexDump) {
312         return decodeHexDump(hexDump, 0, hexDump.length());
313     }
314 
315     /**
316      * Generates a class name from a {@link Class}. Similar to {@link Class#getName()}, but null-safe.
317      */
318     public static String className(Object o) {
319         if (o == null) {
320             return "null_object";
321         } else {
322             return o.getClass().getName();
323         }
324     }
325 
326     /**
327      * The shortcut to {@link #simpleClassName(Class) simpleClassName(o.getClass())}.
328      */
329     public static String simpleClassName(Object o) {
330         if (o == null) {
331             return "null_object";
332         } else {
333             return simpleClassName(o.getClass());
334         }
335     }
336 
337     /**
338      * Generates a simplified name from a {@link Class}.  Similar to {@link Class#getSimpleName()}, but it works fine
339      * with anonymous classes.
340      */
341     public static String simpleClassName(Class<?> clazz) {
342         String className = checkNotNull(clazz, "clazz").getName();
343         final int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
344         if (lastDotIdx > -1) {
345             return className.substring(lastDotIdx + 1);
346         }
347         return className;
348     }
349 
350     /**
351      * Escapes the specified value, if necessary according to
352      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
353      *
354      * @param value The value which will be escaped according to
355      *              <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
356      * @return {@link CharSequence} the escaped value if necessary, or the value unchanged
357      */
358     public static CharSequence escapeCsv(CharSequence value) {
359         return escapeCsv(value, false);
360     }
361 
362     /**
363      * Escapes the specified value, if necessary according to
364      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
365      *
366      * @param value          The value which will be escaped according to
367      *                       <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
368      * @param trimWhiteSpace The value will first be trimmed of its optional white-space characters,
369      *                       according to <a href="https://tools.ietf.org/html/rfc7230#section-7">RFC-7230</a>
370      * @return {@link CharSequence} the escaped value if necessary, or the value unchanged
371      */
372     public static CharSequence escapeCsv(CharSequence value, boolean trimWhiteSpace) {
373         int length = checkNotNull(value, "value").length();
374         int start;
375         int last;
376         if (trimWhiteSpace) {
377             start = indexOfFirstNonOwsChar(value, length);
378             last = indexOfLastNonOwsChar(value, start, length);
379         } else {
380             start = 0;
381             last = length - 1;
382         }
383         if (start > last) {
384             return EMPTY_STRING;
385         }
386 
387         int firstUnescapedSpecial = -1;
388         boolean quoted = false;
389         if (isDoubleQuote(value.charAt(start))) {
390             quoted = isDoubleQuote(value.charAt(last)) && last > start;
391             if (quoted) {
392                 start++;
393                 last--;
394             } else {
395                 firstUnescapedSpecial = start;
396             }
397         }
398 
399         if (firstUnescapedSpecial < 0) {
400             if (quoted) {
401                 for (int i = start; i <= last; i++) {
402                     if (isDoubleQuote(value.charAt(i))) {
403                         if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
404                             firstUnescapedSpecial = i;
405                             break;
406                         }
407                         i++;
408                     }
409                 }
410             } else {
411                 for (int i = start; i <= last; i++) {
412                     char c = value.charAt(i);
413                     if (c == LINE_FEED || c == CARRIAGE_RETURN || c == COMMA) {
414                         firstUnescapedSpecial = i;
415                         break;
416                     }
417                     if (isDoubleQuote(c)) {
418                         if (i == last || !isDoubleQuote(value.charAt(i + 1))) {
419                             firstUnescapedSpecial = i;
420                             break;
421                         }
422                         i++;
423                     }
424                 }
425             }
426 
427             if (firstUnescapedSpecial < 0) {
428                 // Special characters is not found or all of them already escaped.
429                 // In the most cases returns a same string. New string will be instantiated (via StringBuilder)
430                 // only if it really needed. It's important to prevent GC extra load.
431                 return quoted? value.subSequence(start - 1, last + 2) : value.subSequence(start, last + 1);
432             }
433         }
434 
435         StringBuilder result = new StringBuilder(last - start + 1 + CSV_NUMBER_ESCAPE_CHARACTERS);
436         result.append(DOUBLE_QUOTE).append(value, start, firstUnescapedSpecial);
437         for (int i = firstUnescapedSpecial; i <= last; i++) {
438             char c = value.charAt(i);
439             if (isDoubleQuote(c)) {
440                 result.append(DOUBLE_QUOTE);
441                 if (i < last && isDoubleQuote(value.charAt(i + 1))) {
442                     i++;
443                 }
444             }
445             result.append(c);
446         }
447         return result.append(DOUBLE_QUOTE);
448     }
449 
450     /**
451      * Unescapes the specified escaped CSV field, if necessary according to
452      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
453      *
454      * @param value The escaped CSV field which will be unescaped according to
455      *              <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
456      * @return {@link CharSequence} the unescaped value if necessary, or the value unchanged
457      */
458     public static CharSequence unescapeCsv(CharSequence value) {
459         int length = checkNotNull(value, "value").length();
460         if (length == 0) {
461             return value;
462         }
463         int last = length - 1;
464         boolean quoted = isDoubleQuote(value.charAt(0)) && isDoubleQuote(value.charAt(last)) && length != 1;
465         if (!quoted) {
466             validateCsvFormat(value);
467             return value;
468         }
469         StringBuilder unescaped = InternalThreadLocalMap.get().stringBuilder();
470         for (int i = 1; i < last; i++) {
471             char current = value.charAt(i);
472             if (current == DOUBLE_QUOTE) {
473                 if (isDoubleQuote(value.charAt(i + 1)) && (i + 1) != last) {
474                     // Followed by a double-quote but not the last character
475                     // Just skip the next double-quote
476                     i++;
477                 } else {
478                     // Not followed by a double-quote or the following double-quote is the last character
479                     throw newInvalidEscapedCsvFieldException(value, i);
480                 }
481             }
482             unescaped.append(current);
483         }
484         return unescaped.toString();
485     }
486 
487     /**
488      * Unescapes the specified escaped CSV fields according to
489      * <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>.
490      *
491      * @param value A string with multiple CSV escaped fields which will be unescaped according to
492      *              <a href="https://tools.ietf.org/html/rfc4180#section-2">RFC-4180</a>
493      * @return {@link List} the list of unescaped fields
494      */
495     public static List<CharSequence> unescapeCsvFields(CharSequence value) {
496         List<CharSequence> unescaped = new ArrayList<CharSequence>(2);
497         StringBuilder current = InternalThreadLocalMap.get().stringBuilder();
498         boolean quoted = false;
499         int last = value.length() - 1;
500         for (int i = 0; i <= last; i++) {
501             char c = value.charAt(i);
502             if (quoted) {
503                 switch (c) {
504                     case DOUBLE_QUOTE:
505                         if (i == last) {
506                             // Add the last field and return
507                             unescaped.add(current.toString());
508                             return unescaped;
509                         }
510                         char next = value.charAt(++i);
511                         if (next == DOUBLE_QUOTE) {
512                             // 2 double-quotes should be unescaped to one
513                             current.append(DOUBLE_QUOTE);
514                             break;
515                         }
516                         if (next == COMMA) {
517                             // This is the end of a field. Let's start to parse the next field.
518                             quoted = false;
519                             unescaped.add(current.toString());
520                             current.setLength(0);
521                             break;
522                         }
523                         // double-quote followed by other character is invalid
524                         throw newInvalidEscapedCsvFieldException(value, i - 1);
525                     default:
526                         current.append(c);
527                 }
528             } else {
529                 switch (c) {
530                     case COMMA:
531                         // Start to parse the next field
532                         unescaped.add(current.toString());
533                         current.setLength(0);
534                         break;
535                     case DOUBLE_QUOTE:
536                         if (current.length() == 0) {
537                             quoted = true;
538                             break;
539                         }
540                         // double-quote appears without being enclosed with double-quotes
541                         // fall through
542                     case LINE_FEED:
543                         // fall through
544                     case CARRIAGE_RETURN:
545                         // special characters appears without being enclosed with double-quotes
546                         throw newInvalidEscapedCsvFieldException(value, i);
547                     default:
548                         current.append(c);
549                 }
550             }
551         }
552         if (quoted) {
553             throw newInvalidEscapedCsvFieldException(value, last);
554         }
555         unescaped.add(current.toString());
556         return unescaped;
557     }
558 
559     /**
560      * Validate if {@code value} is a valid csv field without double-quotes.
561      *
562      * @throws IllegalArgumentException if {@code value} needs to be encoded with double-quotes.
563      */
564     private static void validateCsvFormat(CharSequence value) {
565         int length = value.length();
566         for (int i = 0; i < length; i++) {
567             switch (value.charAt(i)) {
568                 case DOUBLE_QUOTE:
569                 case LINE_FEED:
570                 case CARRIAGE_RETURN:
571                 case COMMA:
572                     // If value contains any special character, it should be enclosed with double-quotes
573                     throw newInvalidEscapedCsvFieldException(value, i);
574                 default:
575             }
576         }
577     }
578 
579     private static IllegalArgumentException newInvalidEscapedCsvFieldException(CharSequence value, int index) {
580         return new IllegalArgumentException("invalid escaped CSV field: " + value + " index: " + index);
581     }
582 
583     /**
584      * Get the length of a string, {@code null} input is considered {@code 0} length.
585      */
586     public static int length(String s) {
587         return s == null ? 0 : s.length();
588     }
589 
590     /**
591      * Determine if a string is {@code null} or {@link String#isEmpty()} returns {@code true}.
592      */
593     public static boolean isNullOrEmpty(String s) {
594         return s == null || s.isEmpty();
595     }
596 
597     /**
598      * Find the index of the first non-white space character in {@code s} starting at {@code offset}.
599      *
600      * @param seq    The string to search.
601      * @param offset The offset to start searching at.
602      * @return the index of the first non-white space character or &lt;{@code -1} if none was found.
603      */
604     public static int indexOfNonWhiteSpace(CharSequence seq, int offset) {
605         for (; offset < seq.length(); ++offset) {
606             if (!Character.isWhitespace(seq.charAt(offset))) {
607                 return offset;
608             }
609         }
610         return -1;
611     }
612 
613     /**
614      * Find the index of the first white space character in {@code s} starting at {@code offset}.
615      *
616      * @param seq    The string to search.
617      * @param offset The offset to start searching at.
618      * @return the index of the first white space character or &lt;{@code -1} if none was found.
619      */
620     public static int indexOfWhiteSpace(CharSequence seq, int offset) {
621         for (; offset < seq.length(); ++offset) {
622             if (Character.isWhitespace(seq.charAt(offset))) {
623                 return offset;
624             }
625         }
626         return -1;
627     }
628 
629     /**
630      * Determine if {@code c} lies within the range of values defined for
631      * <a href="https://unicode.org/glossary/#surrogate_code_point">Surrogate Code Point</a>.
632      *
633      * @param c the character to check.
634      * @return {@code true} if {@code c} lies within the range of values defined for
635      * <a href="https://unicode.org/glossary/#surrogate_code_point">Surrogate Code Point</a>. {@code false} otherwise.
636      */
637     public static boolean isSurrogate(char c) {
638         return c >= '\uD800' && c <= '\uDFFF';
639     }
640 
641     private static boolean isDoubleQuote(char c) {
642         return c == DOUBLE_QUOTE;
643     }
644 
645     /**
646      * Determine if the string {@code s} ends with the char {@code c}.
647      *
648      * @param s the string to test
649      * @param c the tested char
650      * @return true if {@code s} ends with the char {@code c}
651      */
652     public static boolean endsWith(CharSequence s, char c) {
653         int len = s.length();
654         return len > 0 && s.charAt(len - 1) == c;
655     }
656 
657     /**
658      * Trim optional white-space characters from the specified value,
659      * according to <a href="https://tools.ietf.org/html/rfc7230#section-7">RFC-7230</a>.
660      *
661      * @param value the value to trim
662      * @return {@link CharSequence} the trimmed value if necessary, or the value unchanged
663      */
664     public static CharSequence trimOws(CharSequence value) {
665         final int length = value.length();
666         if (length == 0) {
667             return value;
668         }
669         int start = indexOfFirstNonOwsChar(value, length);
670         int end = indexOfLastNonOwsChar(value, start, length);
671         return start == 0 && end == length - 1 ? value : value.subSequence(start, end + 1);
672     }
673 
674     /**
675      * Returns a char sequence that contains all {@code elements} joined by a given separator.
676      *
677      * @param separator for each element
678      * @param elements to join together
679      *
680      * @return a char sequence joined by a given separator.
681      */
682     public static CharSequence join(CharSequence separator, Iterable<? extends CharSequence> elements) {
683         ObjectUtil.checkNotNull(separator, "separator");
684         ObjectUtil.checkNotNull(elements, "elements");
685 
686         Iterator<? extends CharSequence> iterator = elements.iterator();
687         if (!iterator.hasNext()) {
688             return EMPTY_STRING;
689         }
690 
691         CharSequence firstElement = iterator.next();
692         if (!iterator.hasNext()) {
693             return firstElement;
694         }
695 
696         StringBuilder builder = new StringBuilder(firstElement);
697         do {
698             builder.append(separator).append(iterator.next());
699         } while (iterator.hasNext());
700 
701         return builder;
702     }
703 
704     /**
705      * @return {@code length} if no OWS is found.
706      */
707     private static int indexOfFirstNonOwsChar(CharSequence value, int length) {
708         int i = 0;
709         while (i < length && isOws(value.charAt(i))) {
710             i++;
711         }
712         return i;
713     }
714 
715     /**
716      * @return {@code start} if no OWS is found.
717      */
718     private static int indexOfLastNonOwsChar(CharSequence value, int start, int length) {
719         int i = length - 1;
720         while (i > start && isOws(value.charAt(i))) {
721             i--;
722         }
723         return i;
724     }
725 
726     private static boolean isOws(char c) {
727         return c == SPACE || c == TAB;
728     }
729 
730 }