/src/haproxy/src/hpack-dec.c
Line | Count | Source |
1 | | /* |
2 | | * HPACK decompressor (RFC7541) |
3 | | * |
4 | | * Copyright (C) 2014-2017 Willy Tarreau <willy@haproxy.org> |
5 | | * Copyright (C) 2017 HAProxy Technologies |
6 | | * |
7 | | * Permission is hereby granted, free of charge, to any person obtaining |
8 | | * a copy of this software and associated documentation files (the |
9 | | * "Software"), to deal in the Software without restriction, including |
10 | | * without limitation the rights to use, copy, modify, merge, publish, |
11 | | * distribute, sublicense, and/or sell copies of the Software, and to |
12 | | * permit persons to whom the Software is furnished to do so, subject to |
13 | | * the following conditions: |
14 | | * |
15 | | * The above copyright notice and this permission notice shall be |
16 | | * included in all copies or substantial portions of the Software. |
17 | | * |
18 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
19 | | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES |
20 | | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
21 | | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
22 | | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
23 | | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
24 | | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR |
25 | | * OTHER DEALINGS IN THE SOFTWARE. |
26 | | */ |
27 | | |
28 | | #include <inttypes.h> |
29 | | #include <stdio.h> |
30 | | #include <stdlib.h> |
31 | | #include <string.h> |
32 | | |
33 | | #include <import/ist.h> |
34 | | #include <haproxy/chunk.h> |
35 | | #include <haproxy/global.h> |
36 | | #include <haproxy/h2.h> |
37 | | #include <haproxy/hpack-dec.h> |
38 | | #include <haproxy/hpack-huff.h> |
39 | | #include <haproxy/hpack-tbl.h> |
40 | | #include <haproxy/tools.h> |
41 | | |
42 | | |
43 | | #if defined(DEBUG_HPACK) |
44 | | #define hpack_debug_printf printf |
45 | | #define hpack_debug_hexdump debug_hexdump |
46 | | #else |
47 | 307k | #define hpack_debug_printf(...) do { } while (0) |
48 | 2.43k | #define hpack_debug_hexdump(...) do { } while (0) |
49 | | #endif |
50 | | |
51 | | /* reads a varint from <raw>'s lowest <b> bits and <len> bytes max (raw included). |
52 | | * returns the 32-bit value on success after updating raw_in and len_in. Forces |
53 | | * len_in to (uint32_t)-1 on truncated input. The caller is responsible for |
54 | | * providing a non-zero <len_in> on input. |
55 | | */ |
56 | | static uint32_t get_var_int(const uint8_t **raw_in, uint32_t *len_in, int b) |
57 | 117k | { |
58 | 117k | uint32_t ret = 0; |
59 | 117k | int len = *len_in; |
60 | 117k | const uint8_t *raw = *raw_in; |
61 | 117k | uint32_t v, max = ~0; |
62 | 117k | uint8_t shift = 0; |
63 | | |
64 | 117k | len--; |
65 | 117k | ret = *(raw++) & ((1 << b) - 1); |
66 | 117k | if (ret != (uint32_t)((1 << b) - 1)) |
67 | 112k | goto end; |
68 | | |
69 | 15.2k | do { |
70 | 15.2k | if (!len) |
71 | 180 | goto too_short; |
72 | 15.1k | v = *raw++; |
73 | 15.1k | len--; |
74 | 15.1k | if (v & 127) { // make UBSan happy |
75 | 11.0k | if ((v & 127) > max) |
76 | 39 | goto too_large; |
77 | 10.9k | ret += (v & 127) << shift; |
78 | 10.9k | } |
79 | 15.0k | max >>= 7; |
80 | 15.0k | shift += 7; |
81 | 15.0k | } while (v & 128); |
82 | | |
83 | 117k | end: |
84 | 117k | *raw_in = raw; |
85 | 117k | *len_in = len; |
86 | 117k | return ret; |
87 | | |
88 | 39 | too_large: |
89 | 219 | too_short: |
90 | 219 | *len_in = (uint32_t)-1; |
91 | 219 | return 0; |
92 | 39 | } |
93 | | |
94 | | /* returns the pseudo-header <idx> corresponds to among the following values : |
95 | | * - 0 = unknown, the header's string needs to be used instead |
96 | | * - 1 = ":authority" |
97 | | * - 2 = ":method" |
98 | | * - 3 = ":path" |
99 | | * - 4 = ":scheme" |
100 | | * - 5 = ":status" |
101 | | */ |
102 | | static inline int hpack_idx_to_phdr(uint32_t idx) |
103 | 36.9k | { |
104 | 36.9k | if (idx > 14) |
105 | 31.8k | return 0; |
106 | | |
107 | 5.10k | idx >>= 1; |
108 | 5.10k | idx <<= 2; |
109 | 5.10k | return (0x55554321U >> idx) & 0xF; |
110 | 36.9k | } |
111 | | |
112 | | /* If <idx> designates a static header, returns <in>. Otherwise allocates some |
113 | | * room from chunk <store> to duplicate <in> into it and returns the string |
114 | | * allocated there. In case of allocation failure, returns a string whose |
115 | | * pointer is NULL. |
116 | | */ |
117 | | static inline struct ist hpack_alloc_string(struct buffer *store, uint32_t idx, |
118 | | struct ist in) |
119 | 100k | { |
120 | 100k | struct ist out; |
121 | | |
122 | 100k | if (idx < HPACK_SHT_SIZE) |
123 | 23.0k | return in; |
124 | | |
125 | 77.7k | out.len = in.len; |
126 | 77.7k | out.ptr = chunk_newstr(store); |
127 | 77.7k | if (unlikely(!isttest(out))) |
128 | 8 | return out; |
129 | | |
130 | 77.6k | if (unlikely(store->data + out.len > store->size)) { |
131 | 38 | out.ptr = NULL; |
132 | 38 | return out; |
133 | 38 | } |
134 | | |
135 | 77.6k | store->data += out.len; |
136 | 77.6k | memcpy(out.ptr, in.ptr, out.len); |
137 | 77.6k | return out; |
138 | 77.6k | } |
139 | | |
140 | | /* decode an HPACK frame starting at <raw> for <len> bytes, using the dynamic |
141 | | * headers table <dht>, produces the output into list <list> of <list_size> |
142 | | * entries max, and uses pre-allocated buffer <tmp> for temporary storage (some |
143 | | * list elements will point to it). Some <list> name entries may be made of a |
144 | | * NULL pointer and a len, in which case they will designate a pseudo header |
145 | | * index according to the values returned by hpack_idx_to_phdr() above. The |
146 | | * number of <list> entries used is returned on success, or <0 on failure, with |
147 | | * the opposite one of the HPACK_ERR_* codes. A last element is always zeroed |
148 | | * and is not counted in the number of returned entries. This way the caller |
149 | | * can use list[].n.len == 0 as a marker for the end of list. |
150 | | */ |
151 | | int hpack_decode_frame(struct hpack_dht *dht, const uint8_t *raw, uint32_t len, |
152 | | struct http_hdr *list, int list_size, |
153 | | struct buffer *tmp) |
154 | 2.26k | { |
155 | 2.26k | uint32_t idx; |
156 | 2.26k | uint32_t nlen; |
157 | 2.26k | uint32_t vlen; |
158 | 2.26k | uint8_t huff; |
159 | 2.26k | struct ist name; |
160 | 2.26k | struct ist value; |
161 | 2.26k | int must_index; |
162 | 2.26k | int ret; |
163 | | |
164 | 2.26k | hpack_debug_hexdump(stderr, "[HPACK-DEC] ", (const char *)raw, 0, len); |
165 | | |
166 | 2.26k | chunk_reset(tmp); |
167 | 2.26k | ret = 0; |
168 | 77.7k | while (len) { |
169 | 77.1k | int __maybe_unused code = *raw; /* first byte, only for debugging */ |
170 | | |
171 | 77.1k | must_index = 0; |
172 | 77.1k | if (*raw >= 0x80) { |
173 | | /* indexed header field */ |
174 | 34.9k | if (*raw == 0x80) { |
175 | 4 | hpack_debug_printf("unhandled code 0x%02x (raw=%p, len=%u)\n", *raw, raw, len); |
176 | 4 | ret = -HPACK_ERR_UNKNOWN_OPCODE; |
177 | 4 | goto leave; |
178 | 4 | } |
179 | | |
180 | 34.9k | hpack_debug_printf("%02x: p14: indexed header field : ", code); |
181 | | |
182 | 34.9k | idx = get_var_int(&raw, &len, 7); |
183 | 34.9k | if (len == (uint32_t)-1) { // truncated |
184 | 31 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
185 | 31 | ret = -HPACK_ERR_TRUNCATED; |
186 | 31 | goto leave; |
187 | 31 | } |
188 | | |
189 | 34.8k | hpack_debug_printf(" idx=%u ", idx); |
190 | | |
191 | 34.8k | if (!hpack_valid_idx(dht, idx)) { |
192 | 133 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
193 | 133 | ret = -HPACK_ERR_TOO_LARGE; |
194 | 133 | goto leave; |
195 | 133 | } |
196 | | |
197 | 34.7k | value = hpack_alloc_string(tmp, idx, hpack_idx_to_value(dht, idx)); |
198 | 34.7k | if (!isttest(value)) { |
199 | 15 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
200 | 15 | ret = -HPACK_ERR_TOO_LARGE; |
201 | 15 | goto leave; |
202 | 15 | } |
203 | | |
204 | | /* here we don't index so we can always keep the pseudo header number */ |
205 | 34.7k | name = ist2(NULL, hpack_idx_to_phdr(idx)); |
206 | | |
207 | 34.7k | if (!name.len) { |
208 | 30.8k | name = hpack_alloc_string(tmp, idx, hpack_idx_to_name(dht, idx)); |
209 | 30.8k | if (!isttest(name)) { |
210 | 14 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
211 | 14 | ret = -HPACK_ERR_TOO_LARGE; |
212 | 14 | goto leave; |
213 | 14 | } |
214 | 30.8k | } |
215 | | /* <name> and <value> are now set and point to stable values */ |
216 | 34.7k | } |
217 | 42.1k | else if (*raw >= 0x20 && *raw <= 0x3f) { |
218 | | /* max dyn table size change */ |
219 | 637 | hpack_debug_printf("%02x: p18: dynamic table size update : ", code); |
220 | | |
221 | 637 | if (ret) { |
222 | | /* 7541#4.2.1 : DHT size update must only be at the beginning */ |
223 | 15 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
224 | 15 | ret = -HPACK_ERR_TOO_LARGE; |
225 | 15 | goto leave; |
226 | 15 | } |
227 | | |
228 | 622 | idx = get_var_int(&raw, &len, 5); |
229 | 622 | if (len == (uint32_t)-1) { // truncated |
230 | 21 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
231 | 21 | ret = -HPACK_ERR_TRUNCATED; |
232 | 21 | goto leave; |
233 | 21 | } |
234 | 601 | hpack_debug_printf(" new len=%u\n", idx); |
235 | | |
236 | 601 | if (idx > dht->size) { |
237 | 63 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
238 | 63 | ret = -HPACK_ERR_INVALID_ARGUMENT; |
239 | 63 | goto leave; |
240 | 63 | } |
241 | 538 | continue; |
242 | 601 | } |
243 | 41.5k | else if (!(*raw & (*raw - 0x10))) { |
244 | | /* 0x00, 0x10, and 0x40 (0x20 and 0x80 were already handled above) */ |
245 | | |
246 | | /* literal header field without/never/with incremental indexing -- literal name */ |
247 | 4.60k | if (*raw == 0x00) |
248 | 1.88k | hpack_debug_printf("%02x: p17: literal without indexing : ", code); |
249 | 2.71k | else if (*raw == 0x10) |
250 | 43 | hpack_debug_printf("%02x: p18: literal never indexed : ", code); |
251 | 2.67k | else if (*raw == 0x40) |
252 | 2.67k | hpack_debug_printf("%02x: p16: literal with indexing : ", code); |
253 | | |
254 | 4.60k | if (*raw == 0x40) |
255 | 2.67k | must_index = 1; |
256 | | |
257 | 4.60k | raw++; len--; |
258 | | |
259 | | /* retrieve name */ |
260 | 4.60k | if (!len) { // truncated |
261 | 24 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
262 | 24 | ret = -HPACK_ERR_TRUNCATED; |
263 | 24 | goto leave; |
264 | 24 | } |
265 | | |
266 | 4.57k | huff = *raw & 0x80; |
267 | 4.57k | nlen = get_var_int(&raw, &len, 7); |
268 | 4.57k | if (len == (uint32_t)-1 || len < nlen) { // truncated |
269 | 146 | hpack_debug_printf("##ERR@%d## (truncated): nlen=%d len=%d\n", |
270 | 146 | __LINE__, (int)nlen, (int)len); |
271 | 146 | ret = -HPACK_ERR_TRUNCATED; |
272 | 146 | goto leave; |
273 | 146 | } |
274 | | |
275 | 4.43k | name = ist2(raw, nlen); |
276 | | |
277 | 4.43k | raw += nlen; |
278 | 4.43k | len -= nlen; |
279 | | |
280 | 4.43k | if (huff) { |
281 | 2.53k | char *ntrash = chunk_newstr(tmp); |
282 | 2.53k | if (!ntrash) { |
283 | 3 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
284 | 3 | ret = -HPACK_ERR_TOO_LARGE; |
285 | 3 | goto leave; |
286 | 3 | } |
287 | | |
288 | 2.52k | nlen = huff_dec((const uint8_t *)name.ptr, name.len, ntrash, |
289 | 2.52k | tmp->size - tmp->data); |
290 | 2.52k | if (nlen == (uint32_t)-1) { |
291 | 130 | hpack_debug_printf("2: can't decode huffman.\n"); |
292 | 130 | ret = -HPACK_ERR_HUFFMAN; |
293 | 130 | goto leave; |
294 | 130 | } |
295 | 2.39k | hpack_debug_printf(" [name huff %d->%d] ", (int)name.len, (int)nlen); |
296 | | |
297 | 2.39k | tmp->data += nlen; // make room for the value |
298 | 2.39k | name = ist2(ntrash, nlen); |
299 | 2.39k | } |
300 | | |
301 | | /* retrieve value */ |
302 | 4.29k | if (!len) { // truncated |
303 | 119 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
304 | 119 | ret = -HPACK_ERR_TRUNCATED; |
305 | 119 | goto leave; |
306 | 119 | } |
307 | | |
308 | 4.17k | huff = *raw & 0x80; |
309 | 4.17k | vlen = get_var_int(&raw, &len, 7); |
310 | 4.17k | if (len == (uint32_t)-1 || len < vlen) { // truncated |
311 | 133 | hpack_debug_printf("##ERR@%d## : vlen=%d len=%d\n", |
312 | 133 | __LINE__, (int)vlen, (int)len); |
313 | 133 | ret = -HPACK_ERR_TRUNCATED; |
314 | 133 | goto leave; |
315 | 133 | } |
316 | | |
317 | 4.04k | value = ist2(raw, vlen); |
318 | 4.04k | raw += vlen; |
319 | 4.04k | len -= vlen; |
320 | | |
321 | 4.04k | if (huff) { |
322 | 2.25k | char *vtrash = chunk_newstr(tmp); |
323 | 2.25k | if (!vtrash) { |
324 | 2 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
325 | 2 | ret = -HPACK_ERR_TOO_LARGE; |
326 | 2 | goto leave; |
327 | 2 | } |
328 | | |
329 | 2.25k | vlen = huff_dec((const uint8_t *)value.ptr, value.len, vtrash, |
330 | 2.25k | tmp->size - tmp->data); |
331 | 2.25k | if (vlen == (uint32_t)-1) { |
332 | 64 | hpack_debug_printf("3: can't decode huffman.\n"); |
333 | 64 | ret = -HPACK_ERR_HUFFMAN; |
334 | 64 | goto leave; |
335 | 64 | } |
336 | 2.18k | hpack_debug_printf(" [value huff %d->%d] ", (int)value.len, (int)vlen); |
337 | | |
338 | 2.18k | tmp->data += vlen; // make room for the value |
339 | 2.18k | value = ist2(vtrash, vlen); |
340 | 2.18k | } |
341 | | |
342 | | /* <name> and <value> are correctly filled here */ |
343 | 4.04k | } |
344 | 36.9k | else { |
345 | | /* 0x01..0x0f : literal header field without indexing -- indexed name */ |
346 | | /* 0x11..0x1f : literal header field never indexed -- indexed name */ |
347 | | /* 0x41..0x7f : literal header field with incremental indexing -- indexed name */ |
348 | | |
349 | 36.9k | if (*raw <= 0x0f) |
350 | 1.75k | hpack_debug_printf("%02x: p16: literal without indexing -- indexed name : ", code); |
351 | 35.2k | else if (*raw >= 0x41) |
352 | 34.5k | hpack_debug_printf("%02x: p15: literal with indexing -- indexed name : ", code); |
353 | 697 | else |
354 | 697 | hpack_debug_printf("%02x: p16: literal never indexed -- indexed name : ", code); |
355 | | |
356 | | /* retrieve name index */ |
357 | 36.9k | if (*raw >= 0x41) { |
358 | 34.5k | must_index = 1; |
359 | 34.5k | idx = get_var_int(&raw, &len, 6); |
360 | 34.5k | } |
361 | 2.45k | else |
362 | 2.45k | idx = get_var_int(&raw, &len, 4); |
363 | | |
364 | 36.9k | hpack_debug_printf(" idx=%u ", idx); |
365 | | |
366 | 36.9k | if (len == (uint32_t)-1 || !len) { // truncated |
367 | 165 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
368 | 165 | ret = -HPACK_ERR_TRUNCATED; |
369 | 165 | goto leave; |
370 | 165 | } |
371 | | |
372 | 36.7k | if (!hpack_valid_idx(dht, idx)) { |
373 | 110 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
374 | 110 | ret = -HPACK_ERR_TOO_LARGE; |
375 | 110 | goto leave; |
376 | 110 | } |
377 | | |
378 | | /* retrieve value */ |
379 | 36.6k | huff = *raw & 0x80; |
380 | 36.6k | vlen = get_var_int(&raw, &len, 7); |
381 | 36.6k | if (len == (uint32_t)-1 || len < vlen) { // truncated |
382 | 143 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
383 | 143 | ret = -HPACK_ERR_TRUNCATED; |
384 | 143 | goto leave; |
385 | 143 | } |
386 | | |
387 | 36.5k | value = ist2(raw, vlen); |
388 | 36.5k | raw += vlen; |
389 | 36.5k | len -= vlen; |
390 | | |
391 | 36.5k | if (huff) { |
392 | 3.75k | char *vtrash = chunk_newstr(tmp); |
393 | 3.75k | if (!vtrash) { |
394 | 3 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
395 | 3 | ret = -HPACK_ERR_TOO_LARGE; |
396 | 3 | goto leave; |
397 | 3 | } |
398 | | |
399 | 3.74k | vlen = huff_dec((const uint8_t *)value.ptr, value.len, vtrash, |
400 | 3.74k | tmp->size - tmp->data); |
401 | 3.74k | if (vlen == (uint32_t)-1) { |
402 | 171 | hpack_debug_printf("##ERR@%d## can't decode huffman : ilen=%d osize=%d\n", |
403 | 171 | __LINE__, (int)value.len, |
404 | 171 | (int)(tmp->size - tmp->data)); |
405 | 171 | hpack_debug_hexdump(stderr, "[HUFFMAN] ", value.ptr, 0, value.len); |
406 | 171 | ret = -HPACK_ERR_HUFFMAN; |
407 | 171 | goto leave; |
408 | 171 | } |
409 | 3.57k | tmp->data += vlen; // make room for the value |
410 | 3.57k | value = ist2(vtrash, vlen); |
411 | 3.57k | } |
412 | | |
413 | 36.3k | name = IST_NULL; |
414 | 36.3k | if (!must_index) |
415 | 2.21k | name.len = hpack_idx_to_phdr(idx); |
416 | | |
417 | 36.3k | if (!name.len) { |
418 | 35.1k | name = hpack_alloc_string(tmp, idx, hpack_idx_to_name(dht, idx)); |
419 | 35.1k | if (!isttest(name)) { |
420 | 17 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
421 | 17 | ret = -HPACK_ERR_TOO_LARGE; |
422 | 17 | goto leave; |
423 | 17 | } |
424 | 35.1k | } |
425 | | /* <name> and <value> are correctly filled here */ |
426 | 36.3k | } |
427 | | |
428 | | /* We must not accept empty header names (forbidden by the spec and used |
429 | | * as a list termination). |
430 | | */ |
431 | 75.0k | if (!name.len) { |
432 | 91 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
433 | 91 | ret = -HPACK_ERR_INVALID_ARGUMENT; |
434 | 91 | goto leave; |
435 | 91 | } |
436 | | |
437 | | /* here's what we have here : |
438 | | * - name.len > 0 |
439 | | * - value is filled with either const data or data allocated from tmp |
440 | | * - name.ptr == NULL && !must_index : known pseudo-header #name.len |
441 | | * - name.ptr != NULL || must_index : general header, unknown pseudo-header or index needed |
442 | | */ |
443 | 74.9k | if (ret >= list_size) { |
444 | 4 | hpack_debug_printf("##ERR@%d##\n", __LINE__); |
445 | 4 | ret = -HPACK_ERR_TOO_LARGE; |
446 | 4 | goto leave; |
447 | 4 | } |
448 | | |
449 | 74.9k | list[ret].n = name; |
450 | 74.9k | list[ret].v = value; |
451 | 74.9k | ret++; |
452 | | |
453 | 74.9k | if (must_index && hpack_dht_insert(dht, name, value) < 0) { |
454 | 0 | hpack_debug_printf("failed to find some room in the dynamic table\n"); |
455 | 0 | ret = -HPACK_ERR_DHT_INSERT_FAIL; |
456 | 0 | goto leave; |
457 | 0 | } |
458 | | |
459 | 74.9k | hpack_debug_printf("\e[1;34m%s\e[0m: ", |
460 | 74.9k | isttest(name) ? istpad(trash.area, name).ptr : h2_phdr_to_str(name.len)); |
461 | | |
462 | 74.9k | hpack_debug_printf("\e[1;35m%s\e[0m [mustidx=%d, used=%d] [n=(%p,%d) v=(%p,%d)]\n", |
463 | 74.9k | istpad(trash.area, value).ptr, must_index, |
464 | 74.9k | dht->used, |
465 | 74.9k | name.ptr, (int)name.len, value.ptr, (int)value.len); |
466 | 74.9k | } |
467 | | |
468 | 647 | if (ret >= list_size) { |
469 | 1 | ret = -HPACK_ERR_TOO_LARGE; |
470 | 1 | goto leave; |
471 | 1 | } |
472 | | |
473 | | /* put an end marker */ |
474 | 646 | list[ret].n = list[ret].v = IST_NULL; |
475 | 646 | ret++; |
476 | | |
477 | 2.26k | leave: |
478 | 2.26k | hpack_debug_printf("-- done: ret=%d list_size=%d --\n", (int)ret, (int)list_size); |
479 | 2.26k | return ret; |
480 | 646 | } |