(state: ParserState)
| 1030 | } |
| 1031 | |
| 1032 | function parseDisplayString(state: ParserState): BareItem { |
| 1033 | if (consume(state) !== "%") { |
| 1034 | throw new SyntaxError( |
| 1035 | `Invalid structured field: expected '%' at position ${state.pos - 1}`, |
| 1036 | ); |
| 1037 | } |
| 1038 | if (consume(state) !== '"') { |
| 1039 | throw new SyntaxError( |
| 1040 | `Invalid structured field: expected '"' at position ${state.pos - 1}`, |
| 1041 | ); |
| 1042 | } |
| 1043 | |
| 1044 | const bytes: number[] = []; |
| 1045 | while (!isEof(state)) { |
| 1046 | const c = consume(state); |
| 1047 | |
| 1048 | if (c === '"') { |
| 1049 | // Decode UTF-8 bytes to string |
| 1050 | try { |
| 1051 | const value = UTF8_DECODER.decode(new Uint8Array(bytes)); |
| 1052 | return { type: "displaystring", value }; |
| 1053 | } catch { |
| 1054 | throw new SyntaxError( |
| 1055 | "Invalid structured field: invalid UTF-8 in display string", |
| 1056 | ); |
| 1057 | } |
| 1058 | } else if (c === "%") { |
| 1059 | // Percent-encoded byte |
| 1060 | const hex1 = consume(state); |
| 1061 | const hex2 = consume(state); |
| 1062 | if (!isLcHexDigit(hex1) || !isLcHexDigit(hex2)) { |
| 1063 | throw new SyntaxError( |
| 1064 | `Invalid structured field: invalid percent encoding at position ${ |
| 1065 | state.pos - 2 |
| 1066 | }`, |
| 1067 | ); |
| 1068 | } |
| 1069 | bytes.push(parseInt(hex1 + hex2, 16)); |
| 1070 | } else { |
| 1071 | // Must be allowed unescaped character per RFC 9651: |
| 1072 | // unescaped = %x20-21 / %x23-24 / %x26-5B / %x5D-7E |
| 1073 | // (space, !, #, $, &-[, ]-~) |
| 1074 | // Note: " (0x22) and % (0x25) must be percent-encoded |
| 1075 | // Note: Per conformance tests, \ (0x5C) is also allowed |
| 1076 | const code = c.charCodeAt(0); |
| 1077 | const isAllowed = code === 0x20 || code === 0x21 || // space, ! |
| 1078 | code === 0x23 || code === 0x24 || // #, $ |
| 1079 | (code >= 0x26 && code <= 0x5b) || // &-[ |
| 1080 | (code >= 0x5c && code <= 0x7e); // \-~ (includes \ per conformance tests) |
| 1081 | if (!isAllowed) { |
| 1082 | throw new SyntaxError( |
| 1083 | `Invalid structured field: invalid character in display string at position ${ |
| 1084 | state.pos - 1 |
| 1085 | }`, |
| 1086 | ); |
| 1087 | } |
| 1088 | bytes.push(code); |
| 1089 | } |
no test coverage detected