]>
Commit | Line | Data |
---|---|---|
fabcaa8d ILT |
1 | // Copyright 2012 The Go Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style | |
3 | // license that can be found in the LICENSE file. | |
4 | ||
5 | package http | |
6 | ||
7 | import ( | |
8 | "bufio" | |
dd931d9b | 9 | "bytes" |
5a8ea165 | 10 | "compress/gzip" |
aa8901e9 ILT |
11 | "crypto/rand" |
12 | "fmt" | |
6736ef96 | 13 | "io" |
dd931d9b | 14 | "io/ioutil" |
aa8901e9 ILT |
15 | "os" |
16 | "reflect" | |
fabcaa8d ILT |
17 | "strings" |
18 | "testing" | |
19 | ) | |
20 | ||
21 | func TestBodyReadBadTrailer(t *testing.T) { | |
22 | b := &body{ | |
bae90c98 ILT |
23 | src: strings.NewReader("foobar"), |
24 | hdr: true, // force reading the trailer | |
25 | r: bufio.NewReader(strings.NewReader("")), | |
fabcaa8d ILT |
26 | } |
27 | buf := make([]byte, 7) | |
28 | n, err := b.Read(buf[:3]) | |
29 | got := string(buf[:n]) | |
30 | if got != "foo" || err != nil { | |
31 | t.Fatalf(`first Read = %d (%q), %v; want 3 ("foo")`, n, got, err) | |
32 | } | |
33 | ||
34 | n, err = b.Read(buf[:]) | |
35 | got = string(buf[:n]) | |
36 | if got != "bar" || err != nil { | |
37 | t.Fatalf(`second Read = %d (%q), %v; want 3 ("bar")`, n, got, err) | |
38 | } | |
39 | ||
40 | n, err = b.Read(buf[:]) | |
41 | got = string(buf[:n]) | |
42 | if err == nil { | |
43 | t.Errorf("final Read was successful (%q), expected error from trailer read", got) | |
44 | } | |
45 | } | |
6736ef96 ILT |
46 | |
47 | func TestFinalChunkedBodyReadEOF(t *testing.T) { | |
48 | res, err := ReadResponse(bufio.NewReader(strings.NewReader( | |
49 | "HTTP/1.1 200 OK\r\n"+ | |
50 | "Transfer-Encoding: chunked\r\n"+ | |
51 | "\r\n"+ | |
52 | "0a\r\n"+ | |
53 | "Body here\n\r\n"+ | |
54 | "09\r\n"+ | |
55 | "continued\r\n"+ | |
56 | "0\r\n"+ | |
57 | "\r\n")), nil) | |
58 | if err != nil { | |
59 | t.Fatal(err) | |
60 | } | |
61 | want := "Body here\ncontinued" | |
62 | buf := make([]byte, len(want)) | |
63 | n, err := res.Body.Read(buf) | |
64 | if n != len(want) || err != io.EOF { | |
6736ef96 ILT |
65 | t.Errorf("Read = %v, %v; want %d, EOF", n, err, len(want)) |
66 | } | |
67 | if string(buf) != want { | |
68 | t.Errorf("buf = %q; want %q", buf, want) | |
69 | } | |
70 | } | |
dd931d9b ILT |
71 | |
72 | func TestDetectInMemoryReaders(t *testing.T) { | |
73 | pr, _ := io.Pipe() | |
74 | tests := []struct { | |
75 | r io.Reader | |
76 | want bool | |
77 | }{ | |
78 | {pr, false}, | |
79 | ||
80 | {bytes.NewReader(nil), true}, | |
81 | {bytes.NewBuffer(nil), true}, | |
82 | {strings.NewReader(""), true}, | |
83 | ||
84 | {ioutil.NopCloser(pr), false}, | |
85 | ||
86 | {ioutil.NopCloser(bytes.NewReader(nil)), true}, | |
87 | {ioutil.NopCloser(bytes.NewBuffer(nil)), true}, | |
88 | {ioutil.NopCloser(strings.NewReader("")), true}, | |
89 | } | |
90 | for i, tt := range tests { | |
91 | got := isKnownInMemoryReader(tt.r) | |
92 | if got != tt.want { | |
93 | t.Errorf("%d: got = %v; want %v", i, got, tt.want) | |
94 | } | |
95 | } | |
96 | } | |
aa8901e9 ILT |
97 | |
98 | type mockTransferWriter struct { | |
99 | CalledReader io.Reader | |
100 | WriteCalled bool | |
101 | } | |
102 | ||
103 | var _ io.ReaderFrom = (*mockTransferWriter)(nil) | |
104 | ||
105 | func (w *mockTransferWriter) ReadFrom(r io.Reader) (int64, error) { | |
106 | w.CalledReader = r | |
107 | return io.Copy(ioutil.Discard, r) | |
108 | } | |
109 | ||
110 | func (w *mockTransferWriter) Write(p []byte) (int, error) { | |
111 | w.WriteCalled = true | |
112 | return ioutil.Discard.Write(p) | |
113 | } | |
114 | ||
115 | func TestTransferWriterWriteBodyReaderTypes(t *testing.T) { | |
116 | fileType := reflect.TypeOf(&os.File{}) | |
117 | bufferType := reflect.TypeOf(&bytes.Buffer{}) | |
118 | ||
119 | nBytes := int64(1 << 10) | |
120 | newFileFunc := func() (r io.Reader, done func(), err error) { | |
121 | f, err := ioutil.TempFile("", "net-http-newfilefunc") | |
122 | if err != nil { | |
123 | return nil, nil, err | |
124 | } | |
125 | ||
126 | // Write some bytes to the file to enable reading. | |
127 | if _, err := io.CopyN(f, rand.Reader, nBytes); err != nil { | |
128 | return nil, nil, fmt.Errorf("failed to write data to file: %v", err) | |
129 | } | |
130 | if _, err := f.Seek(0, 0); err != nil { | |
131 | return nil, nil, fmt.Errorf("failed to seek to front: %v", err) | |
132 | } | |
133 | ||
134 | done = func() { | |
135 | f.Close() | |
136 | os.Remove(f.Name()) | |
137 | } | |
138 | ||
139 | return f, done, nil | |
140 | } | |
141 | ||
142 | newBufferFunc := func() (io.Reader, func(), error) { | |
143 | return bytes.NewBuffer(make([]byte, nBytes)), func() {}, nil | |
144 | } | |
145 | ||
146 | cases := []struct { | |
147 | name string | |
148 | bodyFunc func() (io.Reader, func(), error) | |
149 | method string | |
150 | contentLength int64 | |
151 | transferEncoding []string | |
152 | limitedReader bool | |
153 | expectedReader reflect.Type | |
154 | expectedWrite bool | |
155 | }{ | |
156 | { | |
157 | name: "file, non-chunked, size set", | |
158 | bodyFunc: newFileFunc, | |
159 | method: "PUT", | |
160 | contentLength: nBytes, | |
161 | limitedReader: true, | |
162 | expectedReader: fileType, | |
163 | }, | |
164 | { | |
165 | name: "file, non-chunked, size set, nopCloser wrapped", | |
166 | method: "PUT", | |
167 | bodyFunc: func() (io.Reader, func(), error) { | |
168 | r, cleanup, err := newFileFunc() | |
169 | return ioutil.NopCloser(r), cleanup, err | |
170 | }, | |
171 | contentLength: nBytes, | |
172 | limitedReader: true, | |
173 | expectedReader: fileType, | |
174 | }, | |
175 | { | |
176 | name: "file, non-chunked, negative size", | |
177 | method: "PUT", | |
178 | bodyFunc: newFileFunc, | |
179 | contentLength: -1, | |
180 | expectedReader: fileType, | |
181 | }, | |
182 | { | |
183 | name: "file, non-chunked, CONNECT, negative size", | |
184 | method: "CONNECT", | |
185 | bodyFunc: newFileFunc, | |
186 | contentLength: -1, | |
187 | expectedReader: fileType, | |
188 | }, | |
189 | { | |
190 | name: "file, chunked", | |
191 | method: "PUT", | |
192 | bodyFunc: newFileFunc, | |
193 | transferEncoding: []string{"chunked"}, | |
194 | expectedWrite: true, | |
195 | }, | |
196 | { | |
197 | name: "buffer, non-chunked, size set", | |
198 | bodyFunc: newBufferFunc, | |
199 | method: "PUT", | |
200 | contentLength: nBytes, | |
201 | limitedReader: true, | |
202 | expectedReader: bufferType, | |
203 | }, | |
204 | { | |
205 | name: "buffer, non-chunked, size set, nopCloser wrapped", | |
206 | method: "PUT", | |
207 | bodyFunc: func() (io.Reader, func(), error) { | |
208 | r, cleanup, err := newBufferFunc() | |
209 | return ioutil.NopCloser(r), cleanup, err | |
210 | }, | |
211 | contentLength: nBytes, | |
212 | limitedReader: true, | |
213 | expectedReader: bufferType, | |
214 | }, | |
215 | { | |
216 | name: "buffer, non-chunked, negative size", | |
217 | method: "PUT", | |
218 | bodyFunc: newBufferFunc, | |
219 | contentLength: -1, | |
220 | expectedWrite: true, | |
221 | }, | |
222 | { | |
223 | name: "buffer, non-chunked, CONNECT, negative size", | |
224 | method: "CONNECT", | |
225 | bodyFunc: newBufferFunc, | |
226 | contentLength: -1, | |
227 | expectedWrite: true, | |
228 | }, | |
229 | { | |
230 | name: "buffer, chunked", | |
231 | method: "PUT", | |
232 | bodyFunc: newBufferFunc, | |
233 | transferEncoding: []string{"chunked"}, | |
234 | expectedWrite: true, | |
235 | }, | |
236 | } | |
237 | ||
238 | for _, tc := range cases { | |
239 | t.Run(tc.name, func(t *testing.T) { | |
240 | body, cleanup, err := tc.bodyFunc() | |
241 | if err != nil { | |
242 | t.Fatal(err) | |
243 | } | |
244 | defer cleanup() | |
245 | ||
246 | mw := &mockTransferWriter{} | |
247 | tw := &transferWriter{ | |
248 | Body: body, | |
249 | ContentLength: tc.contentLength, | |
250 | TransferEncoding: tc.transferEncoding, | |
251 | } | |
252 | ||
253 | if err := tw.writeBody(mw); err != nil { | |
254 | t.Fatal(err) | |
255 | } | |
256 | ||
257 | if tc.expectedReader != nil { | |
258 | if mw.CalledReader == nil { | |
259 | t.Fatal("did not call ReadFrom") | |
260 | } | |
261 | ||
262 | var actualReader reflect.Type | |
263 | lr, ok := mw.CalledReader.(*io.LimitedReader) | |
264 | if ok && tc.limitedReader { | |
265 | actualReader = reflect.TypeOf(lr.R) | |
266 | } else { | |
267 | actualReader = reflect.TypeOf(mw.CalledReader) | |
268 | } | |
269 | ||
270 | if tc.expectedReader != actualReader { | |
271 | t.Fatalf("got reader %T want %T", actualReader, tc.expectedReader) | |
272 | } | |
273 | } | |
274 | ||
275 | if tc.expectedWrite && !mw.WriteCalled { | |
276 | t.Fatal("did not invoke Write") | |
277 | } | |
278 | }) | |
279 | } | |
280 | } | |
281 | ||
282 | func TestFixTransferEncoding(t *testing.T) { | |
283 | tests := []struct { | |
284 | hdr Header | |
285 | wantErr error | |
286 | }{ | |
287 | { | |
288 | hdr: Header{"Transfer-Encoding": {"fugazi"}}, | |
289 | wantErr: &unsupportedTEError{`unsupported transfer encoding: "fugazi"`}, | |
290 | }, | |
291 | { | |
292 | hdr: Header{"Transfer-Encoding": {"chunked, chunked", "identity", "chunked"}}, | |
5a8ea165 | 293 | wantErr: &badStringError{"chunked must be applied only once, as the last encoding", "chunked, chunked"}, |
aa8901e9 ILT |
294 | }, |
295 | { | |
296 | hdr: Header{"Transfer-Encoding": {"chunked"}}, | |
297 | wantErr: nil, | |
298 | }, | |
299 | } | |
300 | ||
301 | for i, tt := range tests { | |
302 | tr := &transferReader{ | |
303 | Header: tt.hdr, | |
304 | ProtoMajor: 1, | |
305 | ProtoMinor: 1, | |
306 | } | |
307 | gotErr := tr.fixTransferEncoding() | |
308 | if !reflect.DeepEqual(gotErr, tt.wantErr) { | |
309 | t.Errorf("%d.\ngot error:\n%v\nwant error:\n%v\n\n", i, gotErr, tt.wantErr) | |
310 | } | |
311 | } | |
312 | } | |
5a8ea165 ILT |
313 | |
314 | func gzipIt(s string) string { | |
315 | buf := new(bytes.Buffer) | |
316 | gw := gzip.NewWriter(buf) | |
317 | gw.Write([]byte(s)) | |
318 | gw.Close() | |
319 | return buf.String() | |
320 | } | |
321 | ||
322 | func TestUnitTestProxyingReadCloserClosesBody(t *testing.T) { | |
323 | var checker closeChecker | |
324 | buf := new(bytes.Buffer) | |
325 | buf.WriteString("Hello, Gophers!") | |
326 | prc := &proxyingReadCloser{ | |
327 | Reader: buf, | |
328 | Closer: &checker, | |
329 | } | |
330 | prc.Close() | |
331 | ||
332 | read, err := ioutil.ReadAll(prc) | |
333 | if err != nil { | |
334 | t.Fatalf("Read error: %v", err) | |
335 | } | |
336 | if g, w := string(read), "Hello, Gophers!"; g != w { | |
337 | t.Errorf("Read mismatch: got %q want %q", g, w) | |
338 | } | |
339 | ||
340 | if checker.closed != true { | |
341 | t.Fatal("closeChecker.Close was never invoked") | |
342 | } | |
343 | } | |
344 | ||
345 | func TestGzipTransferEncoding_request(t *testing.T) { | |
346 | helloWorldGzipped := gzipIt("Hello, World!") | |
347 | ||
348 | tests := []struct { | |
349 | payload string | |
350 | wantErr string | |
351 | wantBody string | |
352 | }{ | |
353 | ||
354 | { | |
355 | // The case of "chunked" properly applied as the last encoding | |
356 | // and a gzipped request payload that is streamed in 3 parts. | |
357 | payload: `POST / HTTP/1.1 | |
358 | Host: golang.org | |
359 | Transfer-Encoding: gzip, chunked | |
360 | Content-Type: text/html; charset=UTF-8 | |
361 | ||
362 | ` + fmt.Sprintf("%02x\r\n%s\r\n%02x\r\n%s\r\n%02x\r\n%s\r\n0\r\n\r\n", | |
363 | 3, helloWorldGzipped[:3], | |
364 | 5, helloWorldGzipped[3:8], | |
365 | len(helloWorldGzipped)-8, helloWorldGzipped[8:]), | |
366 | wantBody: `Hello, World!`, | |
367 | }, | |
368 | ||
369 | { | |
370 | // The request specifies "Transfer-Encoding: chunked" so its body must be left untouched. | |
371 | payload: `PUT / HTTP/1.1 | |
372 | Host: golang.org | |
373 | Transfer-Encoding: chunked | |
374 | Connection: close | |
375 | Content-Type: text/html; charset=UTF-8 | |
376 | ||
377 | ` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped), | |
378 | // We want that payload as it was sent. | |
379 | wantBody: helloWorldGzipped, | |
380 | }, | |
381 | ||
382 | { | |
383 | // Valid request, the body doesn't have "Transfer-Encoding: chunked" but implicitly encoded | |
384 | // for chunking as per the advisory from RFC 7230 3.3.1 which advises for cases where. | |
385 | payload: `POST / HTTP/1.1 | |
386 | Host: localhost | |
387 | Transfer-Encoding: gzip | |
388 | Content-Type: text/html; charset=UTF-8 | |
389 | ||
390 | ` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped), | |
391 | wantBody: `Hello, World!`, | |
392 | }, | |
393 | ||
394 | { | |
395 | // Invalid request, the body isn't chunked nor is the connection terminated immediately | |
396 | // hence invalid as per the advisory from RFC 7230 3.3.1 which advises for cases where | |
397 | // a Transfer-Encoding that isn't finally chunked is provided. | |
398 | payload: `PUT / HTTP/1.1 | |
399 | Host: golang.org | |
400 | Transfer-Encoding: gzip | |
401 | Content-Length: 0 | |
402 | Connection: close | |
403 | Content-Type: text/html; charset=UTF-8 | |
404 | ||
405 | `, | |
406 | wantErr: `EOF`, | |
407 | }, | |
408 | ||
409 | { | |
410 | // The case of chunked applied before another encoding. | |
411 | payload: `PUT / HTTP/1.1 | |
412 | Location: golang.org | |
413 | Transfer-Encoding: chunked, gzip | |
414 | Content-Length: 0 | |
415 | Connection: close | |
416 | Content-Type: text/html; charset=UTF-8 | |
417 | ||
418 | `, | |
419 | wantErr: `chunked must be applied only once, as the last encoding "chunked, gzip"`, | |
420 | }, | |
421 | ||
422 | { | |
423 | // The case of chunked properly applied as the | |
424 | // last encoding BUT with a bad "Content-Length". | |
425 | payload: `POST / HTTP/1.1 | |
426 | Host: golang.org | |
427 | Transfer-Encoding: gzip, chunked | |
428 | Content-Length: 10 | |
429 | Connection: close | |
430 | Content-Type: text/html; charset=UTF-8 | |
431 | ||
432 | ` + "0\r\n\r\n", | |
433 | wantErr: "EOF", | |
434 | }, | |
435 | } | |
436 | ||
437 | for i, tt := range tests { | |
438 | req, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.payload))) | |
439 | if tt.wantErr != "" { | |
440 | if err == nil || !strings.Contains(err.Error(), tt.wantErr) { | |
441 | t.Errorf("test %d. Error mismatch\nGot: %v\nWant: %s", i, err, tt.wantErr) | |
442 | } | |
443 | continue | |
444 | } | |
445 | ||
446 | if err != nil { | |
447 | t.Errorf("test %d. Unexpected ReadRequest error: %v\nPayload:\n%s", i, err, tt.payload) | |
448 | continue | |
449 | } | |
450 | ||
451 | got, err := ioutil.ReadAll(req.Body) | |
452 | req.Body.Close() | |
453 | if err != nil { | |
454 | t.Errorf("test %d. Failed to read response body: %v", i, err) | |
455 | } | |
456 | if g, w := string(got), tt.wantBody; g != w { | |
457 | t.Errorf("test %d. Request body mimsatch\nGot:\n%s\n\nWant:\n%s", i, g, w) | |
458 | } | |
459 | } | |
460 | } | |
461 | ||
462 | func TestGzipTransferEncoding_response(t *testing.T) { | |
463 | helloWorldGzipped := gzipIt("Hello, World!") | |
464 | ||
465 | tests := []struct { | |
466 | payload string | |
467 | wantErr string | |
468 | wantBody string | |
469 | }{ | |
470 | ||
471 | { | |
472 | // The case of "chunked" properly applied as the last encoding | |
473 | // and a gzipped payload that is streamed in 3 parts. | |
474 | payload: `HTTP/1.1 302 Found | |
475 | Location: https://golang.org/ | |
476 | Transfer-Encoding: gzip, chunked | |
477 | Connection: close | |
478 | Content-Type: text/html; charset=UTF-8 | |
479 | ||
480 | ` + fmt.Sprintf("%02x\r\n%s\r\n%02x\r\n%s\r\n%02x\r\n%s\r\n0\r\n\r\n", | |
481 | 3, helloWorldGzipped[:3], | |
482 | 5, helloWorldGzipped[3:8], | |
483 | len(helloWorldGzipped)-8, helloWorldGzipped[8:]), | |
484 | wantBody: `Hello, World!`, | |
485 | }, | |
486 | ||
487 | { | |
488 | // The response specifies "Transfer-Encoding: chunked" so response body must be left untouched. | |
489 | payload: `HTTP/1.1 302 Found | |
490 | Location: https://golang.org/ | |
491 | Transfer-Encoding: chunked | |
492 | Connection: close | |
493 | Content-Type: text/html; charset=UTF-8 | |
494 | ||
495 | ` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped), | |
496 | // We want that payload as it was sent. | |
497 | wantBody: helloWorldGzipped, | |
498 | }, | |
499 | ||
500 | { | |
501 | // Valid response, the body doesn't have "Transfer-Encoding: chunked" but implicitly encoded | |
502 | // for chunking as per the advisory from RFC 7230 3.3.1 which advises for cases where. | |
503 | payload: `HTTP/1.1 302 Found | |
504 | Location: https://golang.org/ | |
505 | Transfer-Encoding: gzip | |
506 | Connection: close | |
507 | Content-Type: text/html; charset=UTF-8 | |
508 | ||
509 | ` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped), | |
510 | wantBody: `Hello, World!`, | |
511 | }, | |
512 | ||
513 | { | |
514 | // Invalid response, the body isn't chunked nor is the connection terminated immediately | |
515 | // hence invalid as per the advisory from RFC 7230 3.3.1 which advises for cases where | |
516 | // a Transfer-Encoding that isn't finally chunked is provided. | |
517 | payload: `HTTP/1.1 302 Found | |
518 | Location: https://golang.org/ | |
519 | Transfer-Encoding: gzip | |
520 | Content-Length: 0 | |
521 | Connection: close | |
522 | Content-Type: text/html; charset=UTF-8 | |
523 | ||
524 | `, | |
525 | wantErr: `EOF`, | |
526 | }, | |
527 | ||
528 | { | |
529 | // The case of chunked applied before another encoding. | |
530 | payload: `HTTP/1.1 302 Found | |
531 | Location: https://golang.org/ | |
532 | Transfer-Encoding: chunked, gzip | |
533 | Content-Length: 0 | |
534 | Connection: close | |
535 | Content-Type: text/html; charset=UTF-8 | |
536 | ||
537 | `, | |
538 | wantErr: `chunked must be applied only once, as the last encoding "chunked, gzip"`, | |
539 | }, | |
540 | ||
541 | { | |
542 | // The case of chunked properly applied as the | |
543 | // last encoding BUT with a bad "Content-Length". | |
544 | payload: `HTTP/1.1 302 Found | |
545 | Location: https://golang.org/ | |
546 | Transfer-Encoding: gzip, chunked | |
547 | Content-Length: 10 | |
548 | Connection: close | |
549 | Content-Type: text/html; charset=UTF-8 | |
550 | ||
551 | ` + "0\r\n\r\n", | |
552 | wantErr: "EOF", | |
553 | }, | |
554 | ||
555 | { | |
556 | // Including "identity" more than once. | |
557 | payload: `HTTP/1.1 200 OK | |
558 | Location: https://golang.org/ | |
559 | Transfer-Encoding: identity, identity | |
560 | Content-Length: 0 | |
561 | Connection: close | |
562 | Content-Type: text/html; charset=UTF-8 | |
563 | ||
564 | ` + "0\r\n\r\n", | |
565 | wantErr: `"identity" when present must be the only transfer encoding "identity, identity"`, | |
566 | }, | |
567 | } | |
568 | ||
569 | for i, tt := range tests { | |
570 | res, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.payload)), nil) | |
571 | if tt.wantErr != "" { | |
572 | if err == nil || !strings.Contains(err.Error(), tt.wantErr) { | |
573 | t.Errorf("test %d. Error mismatch\nGot: %v\nWant: %s", i, err, tt.wantErr) | |
574 | } | |
575 | continue | |
576 | } | |
577 | ||
578 | if err != nil { | |
579 | t.Errorf("test %d. Unexpected ReadResponse error: %v\nPayload:\n%s", i, err, tt.payload) | |
580 | continue | |
581 | } | |
582 | ||
583 | got, err := ioutil.ReadAll(res.Body) | |
584 | res.Body.Close() | |
585 | if err != nil { | |
586 | t.Errorf("test %d. Failed to read response body: %v", i, err) | |
587 | } | |
588 | if g, w := string(got), tt.wantBody; g != w { | |
589 | t.Errorf("test %d. Response body mimsatch\nGot:\n%s\n\nWant:\n%s", i, g, w) | |
590 | } | |
591 | } | |
592 | } |