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