1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "third_party/blink/renderer/core/loader/resource/multipart_image_resource_parser.h"
6
7 #include <stddef.h>
8 #include <stdint.h>
9 #include <string.h>
10
11 #include "base/stl_util.h"
12 #include "testing/gtest/include/gtest/gtest.h"
13 #include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
14 #include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
15
16 namespace blink {
17 namespace multipart_image_resource_parser_test {
18
ToString(const Vector<char> & data)19 String ToString(const Vector<char>& data) {
20 if (data.IsEmpty())
21 return String("");
22 return String(data.data(), data.size());
23 }
24
25 class MockClient final : public GarbageCollected<MockClient>,
26 public MultipartImageResourceParser::Client {
27 public:
OnePartInMultipartReceived(const ResourceResponse & response)28 void OnePartInMultipartReceived(const ResourceResponse& response) override {
29 responses_.push_back(response);
30 data_.push_back(Vector<char>());
31 }
MultipartDataReceived(const char * bytes,size_t size)32 void MultipartDataReceived(const char* bytes, size_t size) override {
33 data_.back().Append(bytes, SafeCast<wtf_size_t>(size));
34 }
35
36 Vector<ResourceResponse> responses_;
37 Vector<Vector<char>> data_;
38 };
39
TEST(MultipartResponseTest,SkippableLength)40 TEST(MultipartResponseTest, SkippableLength) {
41 struct {
42 const char* input;
43 const wtf_size_t position;
44 const wtf_size_t expected;
45 } line_tests[] = {
46 {"Line", 0, 0}, {"Line", 2, 0}, {"Line", 10, 0},
47 {"\r\nLine", 0, 2}, {"\nLine", 0, 1}, {"\n\nLine", 0, 1},
48 {"\rLine", 0, 0}, {"Line\r\nLine", 4, 2}, {"Line\nLine", 4, 1},
49 {"Line\n\nLine", 4, 1}, {"Line\rLine", 4, 0}, {"Line\r\rLine", 4, 0},
50 };
51 for (size_t i = 0; i < base::size(line_tests); ++i) {
52 Vector<char> input;
53 input.Append(line_tests[i].input,
54 static_cast<wtf_size_t>(strlen(line_tests[i].input)));
55 EXPECT_EQ(line_tests[i].expected,
56 MultipartImageResourceParser::SkippableLengthForTest(
57 input, line_tests[i].position));
58 }
59 }
60
TEST(MultipartResponseTest,FindBoundary)61 TEST(MultipartResponseTest, FindBoundary) {
62 struct {
63 const char* boundary;
64 const char* data;
65 const size_t position;
66 } boundary_tests[] = {
67 {"bound", "bound", 0}, {"bound", "--bound", 0},
68 {"bound", "junkbound", 4}, {"bound", "junk--bound", 4},
69 {"foo", "bound", kNotFound}, {"bound", "--boundbound", 0},
70 };
71
72 for (size_t i = 0; i < base::size(boundary_tests); ++i) {
73 Vector<char> boundary, data;
74 boundary.Append(boundary_tests[i].boundary,
75 static_cast<uint32_t>(strlen(boundary_tests[i].boundary)));
76 data.Append(boundary_tests[i].data,
77 static_cast<uint32_t>(strlen(boundary_tests[i].data)));
78 EXPECT_EQ(
79 boundary_tests[i].position,
80 MultipartImageResourceParser::FindBoundaryForTest(data, &boundary));
81 }
82 }
83
TEST(MultipartResponseTest,NoStartBoundary)84 TEST(MultipartResponseTest, NoStartBoundary) {
85 ResourceResponse response(NullURL());
86 response.SetMimeType("multipart/x-mixed-replace");
87 response.SetHttpHeaderField("Foo", "Bar");
88 response.SetHttpHeaderField("Content-type", "text/plain");
89 MockClient* client = MakeGarbageCollected<MockClient>();
90 Vector<char> boundary;
91 boundary.Append("bound", 5);
92
93 MultipartImageResourceParser* parser =
94 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
95 client);
96 const char kData[] =
97 "Content-type: text/plain\n\n"
98 "This is a sample response\n"
99 "--bound--"
100 "ignore junk after end token --bound\n\nTest2\n";
101 parser->AppendData(kData, strlen(kData));
102 ASSERT_EQ(1u, client->responses_.size());
103 ASSERT_EQ(1u, client->data_.size());
104 EXPECT_EQ("This is a sample response", ToString(client->data_[0]));
105
106 parser->Finish();
107 ASSERT_EQ(1u, client->responses_.size());
108 ASSERT_EQ(1u, client->data_.size());
109 EXPECT_EQ("This is a sample response", ToString(client->data_[0]));
110 }
111
TEST(MultipartResponseTest,NoEndBoundary)112 TEST(MultipartResponseTest, NoEndBoundary) {
113 ResourceResponse response(NullURL());
114 response.SetMimeType("multipart/x-mixed-replace");
115 response.SetHttpHeaderField("Foo", "Bar");
116 response.SetHttpHeaderField("Content-type", "text/plain");
117 MockClient* client = MakeGarbageCollected<MockClient>();
118 Vector<char> boundary;
119 boundary.Append("bound", 5);
120
121 MultipartImageResourceParser* parser =
122 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
123 client);
124 const char kData[] =
125 "bound\nContent-type: text/plain\n\n"
126 "This is a sample response\n";
127 parser->AppendData(kData, strlen(kData));
128 ASSERT_EQ(1u, client->responses_.size());
129 ASSERT_EQ(1u, client->data_.size());
130 EXPECT_EQ("This is a sample ", ToString(client->data_[0]));
131
132 parser->Finish();
133 ASSERT_EQ(1u, client->responses_.size());
134 ASSERT_EQ(1u, client->data_.size());
135 EXPECT_EQ("This is a sample response\n", ToString(client->data_[0]));
136 }
137
TEST(MultipartResponseTest,NoStartAndEndBoundary)138 TEST(MultipartResponseTest, NoStartAndEndBoundary) {
139 ResourceResponse response(NullURL());
140 response.SetMimeType("multipart/x-mixed-replace");
141 response.SetHttpHeaderField("Foo", "Bar");
142 response.SetHttpHeaderField("Content-type", "text/plain");
143 MockClient* client = MakeGarbageCollected<MockClient>();
144 Vector<char> boundary;
145 boundary.Append("bound", 5);
146
147 MultipartImageResourceParser* parser =
148 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
149 client);
150 const char kData[] =
151 "Content-type: text/plain\n\n"
152 "This is a sample response\n";
153 parser->AppendData(kData, strlen(kData));
154 ASSERT_EQ(1u, client->responses_.size());
155 ASSERT_EQ(1u, client->data_.size());
156 EXPECT_EQ("This is a sample ", ToString(client->data_[0]));
157
158 parser->Finish();
159 ASSERT_EQ(1u, client->responses_.size());
160 ASSERT_EQ(1u, client->data_.size());
161 EXPECT_EQ("This is a sample response\n", ToString(client->data_[0]));
162 }
163
TEST(MultipartResponseTest,MalformedBoundary)164 TEST(MultipartResponseTest, MalformedBoundary) {
165 // Some servers send a boundary that is prefixed by "--". See bug 5786.
166 ResourceResponse response(NullURL());
167 response.SetMimeType("multipart/x-mixed-replace");
168 response.SetHttpHeaderField("Foo", "Bar");
169 response.SetHttpHeaderField("Content-type", "text/plain");
170 MockClient* client = MakeGarbageCollected<MockClient>();
171 Vector<char> boundary;
172 boundary.Append("--bound", 7);
173
174 MultipartImageResourceParser* parser =
175 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
176 client);
177 const char kData[] =
178 "--bound\n"
179 "Content-type: text/plain\n\n"
180 "This is a sample response\n"
181 "--bound--"
182 "ignore junk after end token --bound\n\nTest2\n";
183 parser->AppendData(kData, strlen(kData));
184 ASSERT_EQ(1u, client->responses_.size());
185 ASSERT_EQ(1u, client->data_.size());
186 EXPECT_EQ("This is a sample response", ToString(client->data_[0]));
187
188 parser->Finish();
189 ASSERT_EQ(1u, client->responses_.size());
190 ASSERT_EQ(1u, client->data_.size());
191 EXPECT_EQ("This is a sample response", ToString(client->data_[0]));
192 }
193
194 // Used in for tests that break the data in various places.
195 struct TestChunk {
196 const int start_position; // offset in data
197 const int end_position; // end offset in data
198 const size_t expected_responses;
199 const char* expected_data;
200 };
201
VariousChunkSizesTest(const TestChunk chunks[],int chunks_size,size_t responses,int received_data,const char * completed_data)202 void VariousChunkSizesTest(const TestChunk chunks[],
203 int chunks_size,
204 size_t responses,
205 int received_data,
206 const char* completed_data) {
207 const char kData[] =
208 "--bound\n" // 0-7
209 "Content-type: image/png\n\n" // 8-32
210 "datadatadatadatadata" // 33-52
211 "--bound\n" // 53-60
212 "Content-type: image/jpg\n\n" // 61-85
213 "foofoofoofoofoo" // 86-100
214 "--bound--"; // 101-109
215
216 ResourceResponse response(NullURL());
217 response.SetMimeType("multipart/x-mixed-replace");
218 MockClient* client = MakeGarbageCollected<MockClient>();
219 Vector<char> boundary;
220 boundary.Append("bound", 5);
221
222 MultipartImageResourceParser* parser =
223 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
224 client);
225
226 for (int i = 0; i < chunks_size; ++i) {
227 ASSERT_LT(chunks[i].start_position, chunks[i].end_position);
228 parser->AppendData(kData + chunks[i].start_position,
229 chunks[i].end_position - chunks[i].start_position);
230 EXPECT_EQ(chunks[i].expected_responses, client->responses_.size());
231 EXPECT_EQ(
232 String(chunks[i].expected_data),
233 client->data_.size() > 0 ? ToString(client->data_.back()) : String(""));
234 }
235 // Check final state
236 parser->Finish();
237 EXPECT_EQ(responses, client->responses_.size());
238 EXPECT_EQ(completed_data, ToString(client->data_.back()));
239 }
240
241 template <size_t N>
VariousChunkSizesTest(const TestChunk (& chunks)[N],size_t responses,int received_data,const char * completed_data)242 void VariousChunkSizesTest(const TestChunk (&chunks)[N],
243 size_t responses,
244 int received_data,
245 const char* completed_data) {
246 VariousChunkSizesTest(chunks, N, responses, received_data, completed_data);
247 }
248
TEST(MultipartResponseTest,BreakInBoundary)249 TEST(MultipartResponseTest, BreakInBoundary) {
250 // Break in the first boundary
251 const TestChunk kBound1[] = {
252 {0, 4, 0, ""}, {4, 110, 2, "foofoofoofoofoo"},
253 };
254 VariousChunkSizesTest(kBound1, 2, 2, "foofoofoofoofoo");
255
256 // Break in first and second
257 const TestChunk kBound2[] = {
258 {0, 4, 0, ""},
259 {4, 55, 1, "datadatadatad"},
260 {55, 65, 1, "datadatadatadatadata"},
261 {65, 110, 2, "foofoofoofoofoo"},
262 };
263 VariousChunkSizesTest(kBound2, 2, 3, "foofoofoofoofoo");
264
265 // Break in second only
266 const TestChunk kBound3[] = {
267 {0, 55, 1, "datadatadatad"}, {55, 110, 2, "foofoofoofoofoo"},
268 };
269 VariousChunkSizesTest(kBound3, 2, 3, "foofoofoofoofoo");
270 }
271
TEST(MultipartResponseTest,BreakInHeaders)272 TEST(MultipartResponseTest, BreakInHeaders) {
273 // Break in first header
274 const TestChunk kHeader1[] = {
275 {0, 10, 0, ""}, {10, 35, 1, ""}, {35, 110, 2, "foofoofoofoofoo"},
276 };
277 VariousChunkSizesTest(kHeader1, 2, 2, "foofoofoofoofoo");
278
279 // Break in both headers
280 const TestChunk kHeader2[] = {
281 {0, 10, 0, ""},
282 {10, 65, 1, "datadatadatadatadata"},
283 {65, 110, 2, "foofoofoofoofoo"},
284 };
285 VariousChunkSizesTest(kHeader2, 2, 2, "foofoofoofoofoo");
286
287 // Break at end of a header
288 const TestChunk kHeader3[] = {
289 {0, 33, 1, ""},
290 {33, 65, 1, "datadatadatadatadata"},
291 {65, 110, 2, "foofoofoofoofoo"},
292 };
293 VariousChunkSizesTest(kHeader3, 2, 2, "foofoofoofoofoo");
294 }
295
TEST(MultipartResponseTest,BreakInData)296 TEST(MultipartResponseTest, BreakInData) {
297 // All data as one chunk
298 const TestChunk kData1[] = {
299 {0, 110, 2, "foofoofoofoofoo"},
300 };
301 VariousChunkSizesTest(kData1, 2, 2, "foofoofoofoofoo");
302
303 // breaks in data segment
304 const TestChunk kData2[] = {
305 {0, 35, 1, ""},
306 {35, 65, 1, "datadatadatadatadata"},
307 {65, 90, 2, ""},
308 {90, 110, 2, "foofoofoofoofoo"},
309 };
310 VariousChunkSizesTest(kData2, 2, 2, "foofoofoofoofoo");
311
312 // Incomplete send
313 const TestChunk kData3[] = {
314 {0, 35, 1, ""}, {35, 90, 2, ""},
315 };
316 VariousChunkSizesTest(kData3, 2, 2, "foof");
317 }
318
TEST(MultipartResponseTest,SmallChunk)319 TEST(MultipartResponseTest, SmallChunk) {
320 ResourceResponse response(NullURL());
321 response.SetMimeType("multipart/x-mixed-replace");
322 response.SetHttpHeaderField("Content-type", "text/plain");
323 MockClient* client = MakeGarbageCollected<MockClient>();
324 Vector<char> boundary;
325 boundary.Append("bound", 5);
326
327 MultipartImageResourceParser* parser =
328 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
329 client);
330
331 // Test chunks of size 1, 2, and 0.
332 const char kData[] =
333 "--boundContent-type: text/plain\n\n"
334 "\n--boundContent-type: text/plain\n\n"
335 "\n\n--boundContent-type: text/plain\n\n"
336 "--boundContent-type: text/plain\n\n"
337 "end--bound--";
338 parser->AppendData(kData, strlen(kData));
339 ASSERT_EQ(4u, client->responses_.size());
340 ASSERT_EQ(4u, client->data_.size());
341 EXPECT_EQ("", ToString(client->data_[0]));
342 EXPECT_EQ("\n", ToString(client->data_[1]));
343 EXPECT_EQ("", ToString(client->data_[2]));
344 EXPECT_EQ("end", ToString(client->data_[3]));
345
346 parser->Finish();
347 ASSERT_EQ(4u, client->responses_.size());
348 ASSERT_EQ(4u, client->data_.size());
349 EXPECT_EQ("", ToString(client->data_[0]));
350 EXPECT_EQ("\n", ToString(client->data_[1]));
351 EXPECT_EQ("", ToString(client->data_[2]));
352 EXPECT_EQ("end", ToString(client->data_[3]));
353 }
354
TEST(MultipartResponseTest,MultipleBoundaries)355 TEST(MultipartResponseTest, MultipleBoundaries) {
356 // Test multiple boundaries back to back
357 ResourceResponse response(NullURL());
358 response.SetMimeType("multipart/x-mixed-replace");
359 MockClient* client = MakeGarbageCollected<MockClient>();
360 Vector<char> boundary;
361 boundary.Append("bound", 5);
362
363 MultipartImageResourceParser* parser =
364 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
365 client);
366
367 const char kData[] = "--bound\r\n\r\n--bound\r\n\r\nfoofoo--bound--";
368 parser->AppendData(kData, strlen(kData));
369 ASSERT_EQ(2u, client->responses_.size());
370 ASSERT_EQ(2u, client->data_.size());
371 EXPECT_EQ("", ToString(client->data_[0]));
372 EXPECT_EQ("foofoo", ToString(client->data_[1]));
373 }
374
TEST(MultipartResponseTest,EatLeadingLF)375 TEST(MultipartResponseTest, EatLeadingLF) {
376 ResourceResponse response(NullURL());
377 response.SetMimeType("multipart/x-mixed-replace");
378 MockClient* client = MakeGarbageCollected<MockClient>();
379 Vector<char> boundary;
380 boundary.Append("bound", 5);
381
382 const char kData[] =
383 "\n\n\n--bound\n\n\ncontent-type: 1\n\n"
384 "\n\n\n--bound\n\ncontent-type: 2\n\n"
385 "\n\n\n--bound\ncontent-type: 3\n\n";
386 MultipartImageResourceParser* parser =
387 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
388 client);
389
390 for (size_t i = 0; i < strlen(kData); ++i)
391 parser->AppendData(&kData[i], 1);
392 parser->Finish();
393
394 ASSERT_EQ(4u, client->responses_.size());
395 ASSERT_EQ(4u, client->data_.size());
396 EXPECT_EQ(String(), client->responses_[0].HttpHeaderField("content-type"));
397 EXPECT_EQ("", ToString(client->data_[0]));
398 EXPECT_EQ(String(), client->responses_[1].HttpHeaderField("content-type"));
399 EXPECT_EQ("\ncontent-type: 1\n\n\n\n", ToString(client->data_[1]));
400 EXPECT_EQ(String(), client->responses_[2].HttpHeaderField("content-type"));
401 EXPECT_EQ("content-type: 2\n\n\n\n", ToString(client->data_[2]));
402 EXPECT_EQ("3", client->responses_[3].HttpHeaderField("content-type"));
403 EXPECT_EQ("", ToString(client->data_[3]));
404 }
405
TEST(MultipartResponseTest,EatLeadingCRLF)406 TEST(MultipartResponseTest, EatLeadingCRLF) {
407 ResourceResponse response(NullURL());
408 response.SetMimeType("multipart/x-mixed-replace");
409 MockClient* client = MakeGarbageCollected<MockClient>();
410 Vector<char> boundary;
411 boundary.Append("bound", 5);
412
413 const char kData[] =
414 "\r\n\r\n\r\n--bound\r\n\r\n\r\ncontent-type: 1\r\n\r\n"
415 "\r\n\r\n\r\n--bound\r\n\r\ncontent-type: 2\r\n\r\n"
416 "\r\n\r\n\r\n--bound\r\ncontent-type: 3\r\n\r\n";
417 MultipartImageResourceParser* parser =
418 MakeGarbageCollected<MultipartImageResourceParser>(response, boundary,
419 client);
420
421 for (size_t i = 0; i < strlen(kData); ++i)
422 parser->AppendData(&kData[i], 1);
423 parser->Finish();
424
425 ASSERT_EQ(4u, client->responses_.size());
426 ASSERT_EQ(4u, client->data_.size());
427 EXPECT_EQ(String(), client->responses_[0].HttpHeaderField("content-type"));
428 EXPECT_EQ("", ToString(client->data_[0]));
429 EXPECT_EQ(String(), client->responses_[1].HttpHeaderField("content-type"));
430 EXPECT_EQ("\r\ncontent-type: 1\r\n\r\n\r\n\r\n", ToString(client->data_[1]));
431 EXPECT_EQ(String(), client->responses_[2].HttpHeaderField("content-type"));
432 EXPECT_EQ("content-type: 2\r\n\r\n\r\n\r\n", ToString(client->data_[2]));
433 EXPECT_EQ("3", client->responses_[3].HttpHeaderField("content-type"));
434 EXPECT_EQ("", ToString(client->data_[3]));
435 }
436
437 } // namespace multipart_image_resource_parser_test
438 } // namespace blink
439