1 package expo.modules.structuredheaders;
2 
3 import android.util.Base64;
4 
5 import java.nio.CharBuffer;
6 import java.util.ArrayList;
7 import java.util.Arrays;
8 import java.util.Collections;
9 import java.util.LinkedHashMap;
10 import java.util.List;
11 import java.util.Objects;
12 
13 /**
14  * Implementation of the "Structured Field Values" Parser.
15  *
16  * @see <a href=
17  *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#text-parse">Section
18  *      4.2 of draft-ietf-httpbis-header-structure-19</a>
19  */
20 public class Parser {
21 
22     private final CharBuffer input;
23     private final List<Integer> startPositions;
24 
25     /**
26      * Creates {@link Parser} for the given input.
27      *
28      * @param input
29      *            single field line
30      * @throws ParseException
31      *             for non-ASCII characters
32      */
33     public Parser(String input) {
34         this(Collections.singletonList(Objects.requireNonNull(input, "input must not be null")));
35     }
36 
37     /**
38      * Creates {@link Parser} for the given input.
39      *
40      * @param input
41      *            field lines
42      * @throws ParseException
43      *             for non-ASCII characters
44      */
45     public Parser(String... input) {
46         this(Arrays.asList(input));
47     }
48 
49     /**
50      * Creates {@link Parser} for the given input.
51      *
52      * @param fieldLines
53      *            field lines
54      * @throws ParseException
55      *             for non-ASCII characters or empty input
56      */
57     public Parser(Iterable<String> fieldLines) {
58 
59         StringBuilder sb = null;
60         String str = null;
61         List<Integer> startPositions = Collections.emptyList();
62 
63         for (String s : Objects.requireNonNull(fieldLines, "fieldLines must not be null")) {
64             Objects.requireNonNull("field line must not be null", s);
65             if (str == null) {
66                 str = checkASCII(s);
67             } else {
68                 if (sb == null) {
69                     sb = new StringBuilder();
70                     sb.append(str);
71                 }
72                 if (startPositions.size() == 0) {
73                     startPositions = new ArrayList<>();
74                 }
75                 startPositions.add(sb.length());
76                 sb.append(",").append(checkASCII(s));
77             }
78         }
79         if (str == null && sb == null) {
80             throw new ParseException("Empty input", "", 0);
81         }
82         this.input = CharBuffer.wrap(sb != null ? sb : str);
83         this.startPositions = startPositions;
84     }
85 
86     private static String checkASCII(String value) {
87         for (int i = 0; i < value.length(); i++) {
88             char c = value.charAt(i);
89             if (c < 0x00 || c > 0x7f) {
90                 throw new ParseException(String.format("Invalid character in field line at position %d: '%c' (0x%04x) (input: %s)",
91                         i, c, (int) c, value), value, i);
92             }
93         }
94         return value;
95     }
96 
97     private NumberItem<? extends Object> internalParseBareIntegerOrDecimal() {
98         boolean isDecimal = false;
99         int sign = 1;
100         StringBuilder inputNumber = new StringBuilder(20);
101 
102         if (checkNextChar('-')) {
103             sign = -1;
104             advance();
105         }
106 
107         if (!checkNextChar("0123456789")) {
108             throw complaint("Illegal start for Integer or Decimal: '" + input + "'");
109         }
110 
111         boolean done = false;
112         while (hasRemaining() && !done) {
113             char c = peek();
114             if (Utils.isDigit(c)) {
115                 inputNumber.append(c);
116                 advance();
117             } else if (!isDecimal && c == '.') {
118                 if (inputNumber.length() > 12) {
119                     throw complaint("Illegal position for decimal point in Decimal after '" + inputNumber + "'");
120                 }
121                 inputNumber.append(c);
122                 isDecimal = true;
123                 advance();
124             } else {
125                 done = true;
126             }
127             if (inputNumber.length() > (isDecimal ? 16 : 15)) {
128                 backout();
129                 throw complaint((isDecimal ? "Decimal" : "Integer") + " too long: " + inputNumber.length() + " characters");
130             }
131         }
132 
133         if (!isDecimal) {
134             long l = Long.parseLong(inputNumber.toString());
135             return IntegerItem.valueOf(sign * l);
136         } else {
137             int dotPos = inputNumber.indexOf(".");
138             int fracLen = inputNumber.length() - dotPos - 1;
139 
140             if (fracLen < 1) {
141                 backout();
142                 throw complaint("Decimal must not end in '.'");
143             } else if (fracLen == 1) {
144                 inputNumber.append("00");
145             } else if (fracLen == 2) {
146                 inputNumber.append("0");
147             } else if (fracLen > 3) {
148                 backout();
149                 throw complaint("Maximum number of fractional digits is 3, found: " + fracLen + ", in: " + inputNumber);
150             }
151 
152             inputNumber.deleteCharAt(dotPos);
153             long l = Long.parseLong(inputNumber.toString());
154             return DecimalItem.valueOf(sign * l);
155         }
156     }
157 
158     private NumberItem<? extends Object> internalParseIntegerOrDecimal() {
159         NumberItem<? extends Object> result = internalParseBareIntegerOrDecimal();
160         Parameters params = internalParseParameters();
161         return result.withParams(params);
162     }
163 
164     private StringItem internalParseBareString() {
165 
166         if (getOrEOD() != '"') {
167             throw complaint("String must start with double quote: '" + input + "'");
168         }
169 
170         StringBuilder outputString = new StringBuilder(length());
171 
172         while (hasRemaining()) {
173             if (startPositions.contains(position())) {
174                 throw complaint("String crosses field line boundary at position " + position());
175             }
176 
177             char c = get();
178             if (c == '\\') {
179                 c = getOrEOD();
180                 if (c == EOD) {
181                     throw complaint("Incomplete escape sequence at position " + position());
182                 } else if (c != '"' && c != '\\') {
183                     backout();
184                     throw complaint("Invalid escape sequence character '" + c + "' at position " + position());
185                 }
186                 outputString.append(c);
187             } else {
188                 if (c == '"') {
189                     return StringItem.valueOf(outputString.toString());
190                 } else if (c < 0x20 || c >= 0x7f) {
191                     throw complaint("Invalid character in String at position " + position());
192                 } else {
193                     outputString.append(c);
194                 }
195             }
196         }
197 
198         throw complaint("Closing DQUOTE missing");
199     }
200 
201     private StringItem internalParseString() {
202         StringItem result = internalParseBareString();
203         Parameters params = internalParseParameters();
204         return result.withParams(params);
205     }
206 
207     private TokenItem internalParseBareToken() {
208 
209         char c = getOrEOD();
210         if (c != '*' && !Utils.isAlpha(c)) {
211             throw complaint("Token must start with ALPHA or *: '" + input + "'");
212         }
213 
214         StringBuilder outputString = new StringBuilder(length());
215         outputString.append(c);
216 
217         boolean done = false;
218         while (hasRemaining() && !done) {
219             c = peek();
220             if (c <= ' ' || c >= 0x7f || "\"(),;<=>?@[\\]{}".indexOf(c) >= 0) {
221                 done = true;
222             } else {
223                 advance();
224                 outputString.append(c);
225             }
226         }
227 
228         return TokenItem.valueOf(outputString.toString());
229     }
230 
231     private TokenItem internalParseToken() {
232         TokenItem result = internalParseBareToken();
233         Parameters params = internalParseParameters();
234         return result.withParams(params);
235     }
236 
237     private static boolean isBase64Char(char c) {
238         return Utils.isAlpha(c) || Utils.isDigit(c) || c == '+' || c == '/' || c == '=';
239     }
240 
241     private ByteSequenceItem internalParseBareByteSequence() {
242         if (getOrEOD() != ':') {
243             throw complaint("Byte Sequence must start with colon: " + input);
244         }
245 
246         StringBuilder outputString = new StringBuilder(length());
247 
248         boolean done = false;
249         while (hasRemaining() && !done) {
250             char c = get();
251             if (c == ':') {
252                 done = true;
253             } else {
254                 if (!isBase64Char(c)) {
255                     throw complaint("Invalid Byte Sequence Character '" + c + "' at position " + position());
256                 }
257                 outputString.append(c);
258             }
259         }
260 
261         if (!done) {
262             throw complaint("Byte Sequence must end with COLON: '" + outputString + "'");
263         }
264 
265         try {
266             return ByteSequenceItem.valueOf(Base64.decode(outputString.toString(), Base64.DEFAULT));
267         } catch (IllegalArgumentException ex) {
268             throw complaint(ex.getMessage(), ex);
269         }
270     }
271 
272     private ByteSequenceItem internalParseByteSequence() {
273         ByteSequenceItem result = internalParseBareByteSequence();
274         Parameters params = internalParseParameters();
275         return result.withParams(params);
276     }
277 
278     private BooleanItem internalParseBareBoolean() {
279 
280         char c = getOrEOD();
281 
282         if (c == EOD) {
283             throw complaint("Missing data in Boolean");
284         } else if (c != '?') {
285             backout();
286             throw complaint(String.format("Boolean must start with question mark, got '%c'", c));
287         }
288 
289         c = getOrEOD();
290 
291         if (c == EOD) {
292             throw complaint("Missing data in Boolean");
293         } else if (c != '0' && c != '1') {
294             backout();
295             throw complaint(String.format("Expected '0' or '1' in Boolean, found '%c'", c));
296         }
297 
298         return BooleanItem.valueOf(c == '1');
299     }
300 
301     private BooleanItem internalParseBoolean() {
302         BooleanItem result = internalParseBareBoolean();
303         Parameters params = internalParseParameters();
304         return result.withParams(params);
305     }
306 
307     private String internalParseKey() {
308 
309         char c = getOrEOD();
310         if (c == EOD) {
311             throw complaint("Missing data in Key");
312         } else if (c != '*' && !Utils.isLcAlpha(c)) {
313             backout();
314             throw complaint("Key must start with LCALPHA or '*': " + format(c));
315         }
316 
317         StringBuilder result = new StringBuilder();
318         result.append(c);
319 
320         boolean done = false;
321         while (hasRemaining() && !done) {
322             c = peek();
323             if (Utils.isLcAlpha(c) || Utils.isDigit(c) || c == '_' || c == '-' || c == '.' || c == '*') {
324                 result.append(c);
325                 advance();
326             } else {
327                 done = true;
328             }
329         }
330 
331         return result.toString();
332     }
333 
334     private Parameters internalParseParameters() {
335 
336         LinkedHashMap<String, Object> result = new LinkedHashMap<>();
337 
338         boolean done = false;
339         while (hasRemaining() && !done) {
340             char c = peek();
341             if (c != ';') {
342                 done = true;
343             } else {
344                 advance();
345                 removeLeadingSP();
346                 String name = internalParseKey();
347                 Item<? extends Object> value = BooleanItem.valueOf(true);
348                 if (peek() == '=') {
349                     advance();
350                     value = internalParseBareItem();
351                 }
352                 result.put(name, value);
353             }
354         }
355 
356         return Parameters.valueOf(result);
357     }
358 
359     private Item<? extends Object> internalParseBareItem() {
360         if (!hasRemaining()) {
361             throw complaint("Empty string found when parsing Bare Item");
362         }
363 
364         char c = peek();
365         if (Utils.isDigit(c) || c == '-') {
366             return internalParseBareIntegerOrDecimal();
367         } else if (c == '"') {
368             return internalParseBareString();
369         } else if (c == '?') {
370             return internalParseBareBoolean();
371         } else if (c == '*' || Utils.isAlpha(c)) {
372             return internalParseBareToken();
373         } else if (c == ':') {
374             return internalParseBareByteSequence();
375         } else {
376             throw complaint("Unexpected start character in Bare Item: " + format(c));
377         }
378     }
379 
380     private Item<? extends Object> internalParseItem() {
381         Item<? extends Object> result = internalParseBareItem();
382         Parameters params = internalParseParameters();
383         return result.withParams(params);
384     }
385 
386     private ListElement<? extends Object> internalParseItemOrInnerList() {
387         return peek() == '(' ? internalParseInnerList() : internalParseItem();
388     }
389 
390     private List<ListElement<? extends Object>> internalParseOuterList() {
391         List<ListElement<? extends Object>> result = new ArrayList<>();
392 
393         while (hasRemaining()) {
394             result.add(internalParseItemOrInnerList());
395             removeLeadingOWS();
396             if (!hasRemaining()) {
397                 return result;
398             }
399             char c = get();
400             if (c != ',') {
401                 backout();
402                 throw complaint("Expected COMMA in List, got: " + format(c));
403             }
404             removeLeadingOWS();
405             if (!hasRemaining()) {
406                 throw complaint("Found trailing COMMA in List");
407             }
408         }
409 
410         // Won't get here
411         return result;
412     }
413 
414     private List<Item<? extends Object>> internalParseBareInnerList() {
415 
416         char c = getOrEOD();
417         if (c != '(') {
418             throw complaint("Inner List must start with '(': " + input);
419         }
420 
421         List<Item<? extends Object>> result = new ArrayList<>();
422 
423         boolean done = false;
424         while (hasRemaining() && !done) {
425             removeLeadingSP();
426 
427             c = peek();
428             if (c == ')') {
429                 advance();
430                 done = true;
431             } else {
432                 Item<? extends Object> item = internalParseItem();
433                 result.add(item);
434 
435                 c = peek();
436                 if (c == EOD) {
437                     throw complaint("Missing data in Inner List");
438                 } else if (c != ' ' && c != ')') {
439                     throw complaint("Expected SP or ')' in Inner List, got: " + format(c));
440                 }
441             }
442 
443         }
444 
445         if (!done) {
446             throw complaint("Inner List must end with ')': " + input);
447         }
448 
449         return result;
450     }
451 
452     private InnerList internalParseInnerList() {
453         List<Item<? extends Object>> result = internalParseBareInnerList();
454         Parameters params = internalParseParameters();
455         return InnerList.valueOf(result).withParams(params);
456     }
457 
458     private Dictionary internalParseDictionary() {
459 
460         LinkedHashMap<String, ListElement<? extends Object>> result = new LinkedHashMap<>();
461 
462         boolean done = false;
463         while (hasRemaining() && !done) {
464 
465             ListElement<? extends Object> member;
466 
467             String name = internalParseKey();
468 
469             if (peek() == '=') {
470                 advance();
471                 member = internalParseItemOrInnerList();
472             } else {
473                 member = BooleanItem.valueOf(true).withParams(internalParseParameters());
474             }
475 
476             result.put(name, member);
477 
478             removeLeadingOWS();
479             if (hasRemaining()) {
480                 char c = get();
481                 if (c != ',') {
482                     backout();
483                     throw complaint("Expected COMMA in Dictionary, found: " + format(c));
484                 }
485                 removeLeadingOWS();
486                 if (!hasRemaining()) {
487                     throw complaint("Found trailing COMMA in Dictionary");
488                 }
489             } else {
490                 done = true;
491             }
492         }
493 
494         return Dictionary.valueOf(result);
495     }
496 
497     // protected methods unit testing
498 
499     protected static IntegerItem parseInteger(String input) {
500         Parser p = new Parser(input);
501         Item<? extends Object> result = p.internalParseIntegerOrDecimal();
502         if (!(result instanceof IntegerItem)) {
503             throw p.complaint("String parsed as Integer '" + input + "' is a Decimal");
504         } else {
505             p.assertEmpty("Extra characters in string parsed as Integer");
506             return (IntegerItem) result;
507         }
508     }
509 
510     protected static DecimalItem parseDecimal(String input) {
511         Parser p = new Parser(input);
512         Item<? extends Object> result = p.internalParseIntegerOrDecimal();
513         if (!(result instanceof DecimalItem)) {
514             throw p.complaint("String parsed as Decimal '" + input + "' is an Integer");
515         } else {
516             p.assertEmpty("Extra characters in string parsed as Decimal");
517             return (DecimalItem) result;
518         }
519     }
520 
521     // public instance methods
522 
523     /**
524      * Implementation of "Parsing a List"
525      *
526      * @return result of parse as {@link OuterList}.
527      *
528      * @see <a href=
529      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-list">Section
530      *      4.2.1 of draft-ietf-httpbis-header-structure-19</a>
531      */
532     public OuterList parseList() {
533         removeLeadingSP();
534         List<ListElement<? extends Object>> result = internalParseOuterList();
535         removeLeadingSP();
536         assertEmpty("Extra characters in string parsed as List");
537         return OuterList.valueOf(result);
538     }
539 
540     /**
541      * Implementation of "Parsing a Dictionary"
542      *
543      * @return result of parse as {@link Dictionary}.
544      *
545      * @see <a href=
546      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-dictionary">Section
547      *      4.2.2 of draft-ietf-httpbis-header-structure-19</a>
548      */
549     public Dictionary parseDictionary() {
550         removeLeadingSP();
551         Dictionary result = internalParseDictionary();
552         removeLeadingSP();
553         assertEmpty("Extra characters in string parsed as Dictionary");
554         return result;
555     }
556 
557     /**
558      * Implementation of "Parsing an Item"
559      *
560      * @return result of parse as {@link Item}.
561      *
562      * @see <a href=
563      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-item">Section
564      *      4.2.3 of draft-ietf-httpbis-header-structure-19</a>
565      */
566     public Item<? extends Object> parseItem() {
567         removeLeadingSP();
568         Item<? extends Object> result = internalParseItem();
569         removeLeadingSP();
570         assertEmpty("Extra characters in string parsed as Item");
571         return result;
572     }
573 
574     // static public methods
575 
576     /**
577      * Implementation of "Parsing a List" (assuming no extra characters left in
578      * input string)
579      *
580      * @param input
581      *            {@link String} to parse.
582      * @return result of parse as {@link OuterList}.
583      *
584      * @see <a href=
585      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-list">Section
586      *      4.2.1 of draft-ietf-httpbis-header-structure-19</a>
587      */
588     public static OuterList parseList(String input) {
589         Parser p = new Parser(input);
590         List<ListElement<? extends Object>> result = p.internalParseOuterList();
591         p.assertEmpty("Extra characters in string parsed as List");
592         return OuterList.valueOf(result);
593     }
594 
595     /**
596      * Implementation of "Parsing an Item Or Inner List" (assuming no extra
597      * characters left in input string)
598      *
599      * @param input
600      *            {@link String} to parse.
601      * @return result of parse as {@link Item}.
602      *
603      * @see <a href=
604      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-item-or-list">Section
605      *      4.2.1.1 of draft-ietf-httpbis-header-structure-19</a>
606      */
607     public static Parametrizable<? extends Object> parseItemOrInnerList(String input) {
608         Parser p = new Parser(input);
609         ListElement<? extends Object> result = p.internalParseItemOrInnerList();
610         p.assertEmpty("Extra characters in string parsed as Item or Inner List");
611         return result;
612     }
613 
614     /**
615      * Implementation of "Parsing an Inner List" (assuming no extra characters
616      * left in input string)
617      *
618      * @param input
619      *            {@link String} to parse.
620      * @return result of parse as {@link InnerList}.
621      *
622      * @see <a href=
623      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-item-or-list">Section
624      *      4.2.1.2 of draft-ietf-httpbis-header-structure-19</a>
625      */
626     public static InnerList parseInnerList(String input) {
627         Parser p = new Parser(input);
628         InnerList result = p.internalParseInnerList();
629         p.assertEmpty("Extra characters in string parsed as Inner List");
630         return result;
631     }
632 
633     /**
634      * Implementation of "Parsing a Dictionary" (assuming no extra characters
635      * left in input string)
636      *
637      * @param input
638      *            {@link String} to parse.
639      * @return result of parse as {@link Dictionary}.
640      *
641      * @see <a href=
642      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-dictionary">Section
643      *      4.2.2 of draft-ietf-httpbis-header-structure-19</a>
644      */
645     public static Dictionary parseDictionary(String input) {
646         Parser p = new Parser(input);
647         Dictionary result = p.internalParseDictionary();
648         p.assertEmpty("Extra characters in string parsed as Dictionary");
649         return result;
650     }
651 
652     /**
653      * Implementation of "Parsing an Item" (assuming no extra characters left in
654      * input string)
655      *
656      * @param input
657      *            {@link String} to parse.
658      * @return result of parse as {@link Item}.
659      *
660      * @see <a href=
661      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-bare-item">Section
662      *      4.2.3 of draft-ietf-httpbis-header-structure-19</a>
663      */
664     public static Item<? extends Object> parseItem(String input) {
665         Parser p = new Parser(input);
666         Item<? extends Object> result = p.parseItem();
667         p.assertEmpty("Extra characters in string parsed as Item");
668         return result;
669     }
670 
671     /**
672      * Implementation of "Parsing a Bare Item" (assuming no extra characters
673      * left in input string)
674      *
675      * @param input
676      *            {@link String} to parse.
677      * @return result of parse as {@link Item}.
678      *
679      * @see <a href=
680      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-bare-item">Section
681      *      4.2.3.1 of draft-ietf-httpbis-header-structure-19</a>
682      */
683     public static Item<? extends Object> parseBareItem(String input) {
684         Parser p = new Parser(input);
685         Item<? extends Object> result = p.internalParseBareItem();
686         p.assertEmpty("Extra characters in string parsed as Bare Item");
687         return result;
688     }
689 
690     /**
691      * Implementation of "Parsing Parameters" (assuming no extra characters left
692      * in input string)
693      *
694      * @param input
695      *            {@link String} to parse.
696      * @return result of parse as {@link Parameters}.
697      *
698      * @see <a href=
699      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-param">Section
700      *      4.2.3.2 of draft-ietf-httpbis-header-structure-19</a>
701      */
702     public static Parameters parseParameters(String input) {
703         Parser p = new Parser(input);
704         Parameters result = p.internalParseParameters();
705         p.assertEmpty("Extra characters in string parsed as Parameters");
706         return result;
707     }
708 
709     /**
710      * Implementation of "Parsing a Key" (assuming no extra characters left in
711      * input string)
712      *
713      * @param input
714      *            {@link String} to parse.
715      * @return result of parse as {@link String}.
716      *
717      * @see <a href=
718      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-key">Section
719      *      4.2.3.3 of draft-ietf-httpbis-header-structure-19</a>
720      */
721     public static String parseKey(String input) {
722         Parser p = new Parser(input);
723         String result = p.internalParseKey();
724         p.assertEmpty("Extra characters in string parsed as Key");
725         return result;
726     }
727 
728     /**
729      * Implementation of "Parsing an Integer or Decimal" (assuming no extra
730      * characters left in input string)
731      *
732      * @param input
733      *            {@link String} to parse.
734      * @return result of parse as {@link NumberItem}.
735      *
736      * @see <a href=
737      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-number">Section
738      *      4.2.4 of draft-ietf-httpbis-header-structure-19</a>
739      */
740     public static NumberItem<? extends Object> parseIntegerOrDecimal(String input) {
741         Parser p = new Parser(input);
742         NumberItem<? extends Object> result = p.internalParseIntegerOrDecimal();
743         p.assertEmpty("Extra characters in string parsed as Integer or Decimal");
744         return result;
745     }
746 
747     /**
748      * Implementation of "Parsing a String" (assuming no extra characters left
749      * in input string)
750      *
751      * @param input
752      *            {@link String} to parse.
753      * @return result of parse as {@link StringItem}.
754      *
755      * @see <a href=
756      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-string">Section
757      *      4.2.5 of draft-ietf-httpbis-header-structure-19</a>
758      */
759     public static StringItem parseString(String input) {
760         Parser p = new Parser(input);
761         StringItem result = p.internalParseString();
762         p.assertEmpty("Extra characters in string parsed as String");
763         return result;
764     }
765 
766     /**
767      * Implementation of "Parsing a Token" (assuming no extra characters left in
768      * input string)
769      *
770      * @param input
771      *            {@link String} to parse.
772      * @return result of parse as {@link TokenItem}.
773      *
774      * @see <a href=
775      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-token">Section
776      *      4.2.6 of draft-ietf-httpbis-header-structure-19</a>
777      */
778     public static TokenItem parseToken(String input) {
779         Parser p = new Parser(input);
780         TokenItem result = p.internalParseToken();
781         p.assertEmpty("Extra characters in string parsed as Token");
782         return result;
783     }
784 
785     /**
786      * Implementation of "Parsing a Byte Sequence" (assuming no extra characters
787      * left in input string)
788      *
789      * @param input
790      *            {@link String} to parse.
791      * @return result of parse as {@link ByteSequenceItem}.
792      *
793      * @see <a href=
794      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-binary">Section
795      *      4.2.7 of draft-ietf-httpbis-header-structure-19</a>
796      */
797     public static ByteSequenceItem parseByteSequence(String input) {
798         Parser p = new Parser(input);
799         ByteSequenceItem result = p.internalParseByteSequence();
800         p.assertEmpty("Extra characters in string parsed as Byte Sequence");
801         return result;
802     }
803 
804     /**
805      * Implementation of "Parsing a Boolean" (assuming no extra characters left
806      * in input string)
807      *
808      * @param input
809      *            {@link String} to parse.
810      * @return result of parse as {@link BooleanItem}.
811      *
812      * @see <a href=
813      *      "https://greenbytes.de/tech/webdav/draft-ietf-httpbis-header-structure-19.html#parse-boolean">Section
814      *      4.2.8 of draft-ietf-httpbis-header-structure-19</a>
815      */
816     public static BooleanItem parseBoolean(String input) {
817         Parser p = new Parser(input);
818         BooleanItem result = p.internalParseBoolean();
819         p.assertEmpty("Extra characters at position %d in string parsed as Boolean: '%s'");
820         return result;
821     }
822 
823     // utility methods on CharBuffer
824 
825     private static char EOD = (char) -1;
826 
827     private void assertEmpty(String message) {
828         if (hasRemaining()) {
829             throw complaint(String.format(message, position(), input));
830         }
831     }
832 
833     private void advance() {
834         input.position(1 + input.position());
835     }
836 
837     private void backout() {
838         input.position(-1 + input.position());
839     }
840 
841     private boolean checkNextChar(char c) {
842         return hasRemaining() && input.charAt(0) == c;
843     }
844 
845     private boolean checkNextChar(String valid) {
846         return hasRemaining() && valid.indexOf(input.charAt(0)) >= 0;
847     }
848 
849     private char get() {
850         return input.get();
851     }
852 
853     private char getOrEOD() {
854         return hasRemaining() ? get() : EOD;
855     }
856 
857     private boolean hasRemaining() {
858         return input.hasRemaining();
859     }
860 
861     private int length() {
862         return input.length();
863     }
864 
865     private char peek() {
866         return hasRemaining() ? input.charAt(0) : EOD;
867     }
868 
869     private int position() {
870         return input.position();
871     }
872 
873     private void removeLeadingSP() {
874         while (checkNextChar(' ')) {
875             advance();
876         }
877     }
878 
879     private void removeLeadingOWS() {
880         while (checkNextChar(" \t")) {
881             advance();
882         }
883     }
884 
885     private ParseException complaint(String message) {
886         return new ParseException(message, input);
887     }
888 
889     private ParseException complaint(String message, Throwable cause) {
890         return new ParseException(message, input, cause);
891     }
892 
893     private static String format(char c) {
894         String s;
895         if (c == 9) {
896             s = "HTAB";
897         } else {
898             s = "'" + c + "'";
899         }
900         return String.format("%s (\\u%04x)", s, (int) c);
901     }
902 }
903