View Javadoc
1   /*
2    * Copyright 2026 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.handler.codec.http;
17  
18  import io.netty.util.ByteProcessor;
19  
20  import java.util.BitSet;
21  
22  /**
23   * Validates the chunk start line. That is, the chunk size and chunk extensions, until the CR LF pair.
24   * See <a href="https://www.rfc-editor.org/rfc/rfc9112#name-chunked-transfer-coding">RFC 9112 section 7.1</a>.
25   *
26   * <pre>{@code
27   *   chunked-body   = *chunk
28   *                    last-chunk
29   *                    trailer-section
30   *                    CRLF
31   *
32   *   chunk          = chunk-size [ chunk-ext ] CRLF
33   *                    chunk-data CRLF
34   *   chunk-size     = 1*HEXDIG
35   *   last-chunk     = 1*("0") [ chunk-ext ] CRLF
36   *
37   *   chunk-data     = 1*OCTET ; a sequence of chunk-size octets
38   *   chunk-ext      = *( BWS ";" BWS chunk-ext-name
39   *                       [ BWS "=" BWS chunk-ext-val ] )
40   *
41   *   chunk-ext-name = token
42   *   chunk-ext-val  = token / quoted-string
43   *   quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
44   *   qdtext         = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
45   *   quoted-pair    = "\" ( HTAB / SP / VCHAR / obs-text )
46   *   obs-text       = %x80-FF
47   *   OWS            = *( SP / HTAB )
48   *                  ; optional whitespace
49   *   BWS            = OWS
50   *                  ; "bad" whitespace
51   *   VCHAR          =  %x21-7E
52   *                  ; visible (printing) characters
53   * }</pre>
54   */
55  final class HttpChunkLineValidatingByteProcessor implements ByteProcessor {
56      private static final int SIZE = 0;
57      private static final int CHUNK_EXT_NAME = 1;
58      private static final int CHUNK_EXT_VAL_START = 2;
59      private static final int CHUNK_EXT_VAL_QUOTED = 3;
60      private static final int CHUNK_EXT_VAL_QUOTED_ESCAPE = 4;
61      private static final int CHUNK_EXT_VAL_QUOTED_END = 5;
62      private static final int CHUNK_EXT_VAL_TOKEN = 6;
63  
64      static final class Match extends BitSet {
65          private static final long serialVersionUID = 49522994383099834L;
66          private final int then;
67  
68          Match(int then) {
69              super(256);
70              this.then = then;
71          }
72  
73          Match chars(String chars) {
74              return chars(chars, true);
75          }
76  
77          Match chars(String chars, boolean value) {
78              for (int i = 0, len = chars.length(); i < len; i++) {
79                  set(chars.charAt(i), value);
80              }
81              return this;
82          }
83  
84          Match range(int from, int to) {
85              return range(from, to, true);
86          }
87  
88          Match range(int from, int to, boolean value) {
89              for (int i = from; i <= to; i++) {
90                  set(i, value);
91              }
92              return this;
93          }
94      }
95  
96      private enum State {
97          Size(
98                  new Match(SIZE).chars("0123456789abcdefABCDEF \t"),
99                  new Match(CHUNK_EXT_NAME).chars(";")),
100         ChunkExtName(
101                 new Match(CHUNK_EXT_NAME)
102                         .range(0x21, 0x7E)
103                         .chars(" \t")
104                         .chars("(),/:<=>?@[\\]{}", false),
105                 new Match(CHUNK_EXT_VAL_START).chars("=")),
106         ChunkExtValStart(
107                 new Match(CHUNK_EXT_VAL_START).chars(" \t"),
108                 new Match(CHUNK_EXT_VAL_QUOTED).chars("\""),
109                 new Match(CHUNK_EXT_VAL_TOKEN)
110                         .range(0x21, 0x7E)
111                         .chars("(),/:<=>?@[\\]{}", false)),
112         ChunkExtValQuoted(
113                 new Match(CHUNK_EXT_VAL_QUOTED_ESCAPE).chars("\\"),
114                 new Match(CHUNK_EXT_VAL_QUOTED_END).chars("\""),
115                 new Match(CHUNK_EXT_VAL_QUOTED)
116                         .chars("\t !")
117                         .range(0x23, 0x5B)
118                         .range(0x5D, 0x7E)
119                         .range(0x80, 0xFF)),
120         ChunkExtValQuotedEscape(
121                 new Match(CHUNK_EXT_VAL_QUOTED)
122                         .chars("\t ")
123                         .range(0x21, 0x7E)
124                         .range(0x80, 0xFF)),
125         ChunkExtValQuotedEnd(
126                 new Match(CHUNK_EXT_VAL_QUOTED_END).chars("\t "),
127                 new Match(CHUNK_EXT_NAME).chars(";")),
128         ChunkExtValToken(
129                 new Match(CHUNK_EXT_VAL_TOKEN)
130                         .range(0x21, 0x7E, true)
131                         .chars("(),/:<=>?@[\\]{}", false),
132                 new Match(CHUNK_EXT_NAME).chars(";")),
133         ;
134 
135         private final Match[] matches;
136 
137         State(Match... matches) {
138             this.matches = matches;
139         }
140 
141         State match(byte value) {
142             for (Match match : matches) {
143                 if (match.get(value)) {
144                     return STATES_BY_ORDINAL[match.then];
145                 }
146             }
147             if (this == Size) {
148                 throw new NumberFormatException("Invalid chunk size");
149             } else {
150                 throw new InvalidChunkExtensionException("Invalid chunk extension");
151             }
152         }
153     }
154 
155     private static final State[] STATES_BY_ORDINAL = State.values();
156 
157     private State state = State.Size;
158 
159     @Override
160     public boolean process(byte value) {
161         state = state.match(value);
162         return true;
163     }
164 
165     public void finish() {
166         if (state != State.Size && state != State.ChunkExtName && state != State.ChunkExtValQuotedEnd) {
167             throw new InvalidChunkExtensionException("Invalid chunk extension");
168         }
169     }
170 }